<?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: Arnaud Joubay</title>
    <description>The latest articles on DEV Community by Arnaud Joubay (@sowenjub).</description>
    <link>https://dev.to/sowenjub</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%2F341695%2Ffb19884f-f417-476f-8a0c-3a9ffe1a814b.jpeg</url>
      <title>DEV Community: Arnaud Joubay</title>
      <link>https://dev.to/sowenjub</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sowenjub"/>
    <language>en</language>
    <item>
      <title>How to add a Diagnostic Mode using Settings.bundle and SwiftUI</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Wed, 27 Apr 2022 09:39:54 +0000</pubDate>
      <link>https://dev.to/sowenjub/how-to-add-a-diagnostic-mode-using-settingsbundle-and-swiftui-a7k</link>
      <guid>https://dev.to/sowenjub/how-to-add-a-diagnostic-mode-using-settingsbundle-and-swiftui-a7k</guid>
      <description>&lt;p&gt;Technical stuff shouldn't surface in your app, but it can be useful at times: let's see how we can enable a "Diagnostic Mode" or a "Developer" menu in your app using Settings.bundle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--j4Q32mi2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/CleanShot-2022-04-27-at-14.40.05.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--j4Q32mi2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/CleanShot-2022-04-27-at-14.40.05.png" alt="" width="736" height="812"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Fight your inner geek
&lt;/h1&gt;

&lt;p&gt;Before diving into the instructions, here are some personal insights explaining why I needed this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xkcd.com/1343/"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ccZL9GMF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://imgs.xkcd.com/comics/manuals.png" alt="XKCD about solving vs creating problems" width="587" height="187"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I get it, &lt;em&gt;you love&lt;/em&gt; tweaking everything, and your app should let users gut it open and dissect everything.&lt;/p&gt;

&lt;p&gt;But for the average user, &lt;em&gt;anything technical can create confusion and complexity&lt;/em&gt;. Our job as software developers is to solve problems, not create new ones. And by offering too many options, you're creating a burden for your users: they have to understand what these options are for, figure out if/when they need them and learn how to use them.&lt;/p&gt;

&lt;p&gt;Even as a tech-savvy user, I don't want to be bothered with too many options inside the app itself if it's not part of the experience.&lt;/p&gt;

&lt;p&gt;My personal rule is that &lt;strong&gt;any menu item that users are unlikely to need shouldn't appear in the in-app Settings.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For instance, my app has a hidden "iCloud Sync" menu option in its Settings to help identify potential iCloud sync issues.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yUPYBLBC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/Simulator-Screen-Shot---iPhone-13-mini---2022-04-27-at-10.33.19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yUPYBLBC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/Simulator-Screen-Shot---iPhone-13-mini---2022-04-27-at-10.33.19.png" alt="" width="880" height="1907"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--c0vtKH63--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/Simulator-Screen-Shot---iPhone-12-Pro---2021-09-22-at-22.10.09-3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--c0vtKH63--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/Simulator-Screen-Shot---iPhone-12-Pro---2021-09-22-at-22.10.09-3.png" alt="" width="880" height="1904"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pV5K-eto--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/Simulator-Screen-Shot---iPhone-12-Pro---2021-09-22-at-22.10.12-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pV5K-eto--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/Simulator-Screen-Shot---iPhone-12-Pro---2021-09-22-at-22.10.12-2.png" alt="" width="880" height="1904"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until now, this menu appeared automatically based on the connectivity status (which I get from &lt;a href="https://github.com/ggruen/CloudKitSyncMonitor"&gt;CloudKitSyncMonitor&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;But there are 2 drawbacks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;it can be disturbing or anxiety-inducing&lt;/strong&gt; : sometimes the menu will appear for a split second during the course of a normal event, and if you don't know what this is about it can be troubling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;you can't access it willingly&lt;/strong&gt; : I added a "Data integrity" section that is not directly related to CloudKit's connectivity status and I want to be able to access it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I wanted to have a way to reveal this menu manually, and this is where Settings.bundle comes in.&lt;/p&gt;

&lt;p&gt;This means I will also be able to add other menu options that could help me collect data, like the entire SQLite store if need be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By adding a toggle in the app Settings (the ones inside the device Settings, not the in-app Settings), where (almost) no one ever goes looking.&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Adding Settings.bundle
&lt;/h1&gt;

&lt;p&gt;I'm only going to cover adding a "Diagnostic Mode" toggle, I'm sure you can figure out the rest from there.&lt;/p&gt;

&lt;p&gt;First, add a new file (⌘N) and look for "Settings Bundle"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EdEX6x1N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/CleanShot-2022-04-27-at-10.51.10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EdEX6x1N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/CleanShot-2022-04-27-at-10.51.10.png" alt="" width="880" height="626"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Snapshot of template picker&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This will add the Settings.bundle which includes a default Root.plist file and a localization file.&lt;/p&gt;

&lt;p&gt;In the "Preference Items":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep only the "Toggle Switch"&lt;/li&gt;
&lt;li&gt;change the &lt;code&gt;Title&lt;/code&gt; to "Diagnostic Mode"&lt;/li&gt;
&lt;li&gt;change the &lt;code&gt;Identifier&lt;/code&gt; to "diagnostic_mode_enabled"&lt;/li&gt;
&lt;li&gt;set the &lt;code&gt;Default Value&lt;/code&gt; to NO so that it's &lt;code&gt;false&lt;/code&gt; by default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--v76e0ZIN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/CleanShot-2022-04-27-at-10.52.47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--v76e0ZIN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://sowenjub.me/writes/content/images/2022/04/CleanShot-2022-04-27-at-10.52.47.png" alt="" width="880" height="158"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Target Root.plist&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's see if this value is available to us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add a breakpoint somewhere in your app &lt;/li&gt;
&lt;li&gt;run it (⌘R)&lt;/li&gt;
&lt;li&gt;get into the console&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;po UserDefaults.standard.bool(forKey: "diagnostic_mode_enabled")&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;false&lt;/code&gt;, it works 🥳&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WRONG.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go back and now enter&lt;code&gt;po UserDefaults.standard.dictionaryRepresentation().keys.sorted()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The&lt;/strong&gt; &lt;code&gt;diagnostic_mode_enabled&lt;/code&gt; &lt;strong&gt;key is missing 😒.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You only got &lt;code&gt;false&lt;/code&gt; because &lt;code&gt;bool(forKey:)&lt;/code&gt; returns false if the key doesn't exist, and not because &lt;code&gt;Default Value&lt;/code&gt; is "NO".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the catch:&lt;/strong&gt; that key won't be available until the user changes the setting.&lt;/p&gt;

&lt;p&gt;So you'd better be careful about the default values you use there. In our case, since we want false by default (including if the user didn't touch the settings), NO is perfect.&lt;/p&gt;
&lt;h1&gt;
  
  
  Showing the button in your SwiftUI view
&lt;/h1&gt;

&lt;p&gt;The only thing you have to do is to listen to the &lt;code&gt;UserDefaults.didChangeNotification&lt;/code&gt; and toggle a @State variable to show or hide your Button or NavigationLink.&lt;/p&gt;

&lt;p&gt;It should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;struct SettingsSection: View {
    @State var isShowingCloudLink = false

    var body: some View {
        Section {
            if isShowingCloudLink {
                NavigationLink(destination: CloudSettingsView()) {
                    Label(…)
                }
            }
        }
        .onReceive(
            NotificationCenter.default
                .publisher(for: UserDefaults.didChangeNotification)
                .receive(on: RunLoop.main)
        ) { _ in
            isShowingCloudLink = UserDefaults.standard.bool(forKey: "diagnostic_mode_enabled")
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Code Sample of the SeriousSection in my Settings&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Don't forget to receive on &lt;code&gt;RunLoop.main&lt;/code&gt; or you will get a notification from Xcode warning that you are "Publishing changes from background threads".&lt;/p&gt;

&lt;p&gt;🍒 As suggested by &lt;a href="https://twitter.com/alpennec/status/1519276975302529025"&gt;@alpennec&lt;/a&gt;, it's a good idea to wrap the &lt;code&gt;isShowingCloudLink = UserDefaults.standard.bool(forKey: "diagnostic_mode_enabled")&lt;/code&gt; in a `withAnimation {}.&lt;/p&gt;

&lt;h1&gt;
  
  
  Localizing the Settings
&lt;/h1&gt;

&lt;p&gt;If your app is available in multiple languages, you'll probably want to localize the settings.&lt;/p&gt;

&lt;p&gt;Unfortunately, this has to be done manually (instead of the usual box-checking from the File Inspector)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Right-click on "en.lproj" and select "Show in Finder"&lt;/li&gt;
&lt;li&gt;Duplicate and rename "en.lproj" for each language you support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the Settings.bundle is added as a reference, you don't have to drag them into Xcode, they will appear automatically.&lt;/p&gt;

&lt;p&gt;If you're using &lt;a href="https://localazy.com/register?ref=aAHPjuDt3H4D-eth"&gt;Localazy&lt;/a&gt; like me, here's what the config for your &lt;code&gt;localazy.json&lt;/code&gt; could look like (more about this &lt;a href="https://sowenjub.me/writes/translating-your-ios-app-with-localazy/"&gt;here&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
{&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"upload": {
    "files": [
        {
            "type": "ios-strings",
            "pattern": "YourApp/Settings.bundle/en.lproj/Root.strings",
            "path": "YourApp/Settings.bundle"
        }
    ]
},

"download": {
    "files": [
        {
          "conditions": [["!startsWith: fastlane, ${path}"]],
          "output": "${path}/${iosLprojFolder}/${file}"
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  🫵 Enjoyed this article?
&lt;/h1&gt;

&lt;p&gt;If you liked this article or want to talk, please like/RT/comment this tweet:&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;
    &lt;div class="ltag__twitter-tweet__media ltag__twitter-tweet__media__two-pics"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--V9ULCDOf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/media/FRWLSWUVkAA84yv.jpg" alt="unknown tweet media content"&gt;
    &lt;/div&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--RVuqwWRh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/3007995101/803f74b91b823a7d36db17ee53538713_normal.jpeg" alt="Arnaud Joubay 🥐🐮 profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Arnaud Joubay 🥐🐮
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        &lt;a class="mentioned-user" href="https://dev.to/sowenjub"&gt;@sowenjub&lt;/a&gt;
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ir1kO05j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      How to add a "Diagnostic Mode" in your &lt;a href="https://twitter.com/hashtag/SwiftUI"&gt;#SwiftUI&lt;/a&gt; app?&lt;br&gt;&lt;br&gt;My "iCloud Sync" menu appears automatically if there's an issue but I wanted something that could be triggered manually and wrote a blog about it.&lt;br&gt;&lt;br&gt;Here are the highlights, a catch, and a link to the article at the end 👇 
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      11:13 AM - 27 Apr 2022
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1519273609457414144" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fFnoeFxk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-reply-action-238fe0a37991706a6880ed13941c3efd6b371e4aefe288fe8e0db85250708bc4.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1519273609457414144" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k6dcrOn8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-retweet-action-632c83532a4e7de573c5c08dbb090ee18b348b13e2793175fea914827bc42046.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/like?tweet_id=1519273609457414144" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SRQc9lOp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-like-action-1ea89f4b87c7d37465b0eb78d51fcb7fe6c03a089805d7ea014ba71365be5171.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


</description>
      <category>swift</category>
      <category>ios</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Translating your iOS app with Localazy</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Fri, 28 May 2021 22:00:27 +0000</pubDate>
      <link>https://dev.to/sowenjub/translating-your-ios-app-with-localazy-53h8</link>
      <guid>https://dev.to/sowenjub/translating-your-ios-app-with-localazy-53h8</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5E-EP0ft--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/Blog-Covers.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5E-EP0ft--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/Blog-Covers.png" alt="Translating your iOS app with Localazy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A couple of people have asked me to translate &lt;a href="https://nomeat.today"&gt;No Meat Today&lt;/a&gt; in their language, but the last time I tried to tackle that, none of the solutions I found were satisfying or in my price range.&lt;/p&gt;

&lt;p&gt;I just found Localazy and I'm sold. And not &lt;em&gt;just&lt;/em&gt; because there’s /lazy/ in the name. It’s built by devs and from the CLI to the way it works, it felt like we understood each other and it was behaving “as it should”.&lt;/p&gt;

&lt;p&gt;So here’s a little recount of my experience so far, hopefully that’ll help you get started.&lt;br&gt;&lt;br&gt;
If you can’t wait to get started, you can register using &lt;a href="https://localazy.com/register?ref=aAHPjuSXTN5m"&gt;this link&lt;/a&gt; and I’ll earn extra phrases (thanks 😁).&lt;/p&gt;
&lt;h1&gt;
  
  
  What needs translation?
&lt;/h1&gt;

&lt;p&gt;In order to get &lt;a href="https://nomeat.today"&gt;No Meat Today&lt;/a&gt; ready for a new country, I have to translate these file formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;strings&lt;/em&gt;: until SwiftUI arrived, I would split Localizable.strings into multiple files and use &lt;a href="%5BApple%20Developer%20Documentation%5D(https://developer.apple.com/documentation/foundation/1418095-nslocalizedstring)"&gt;table names&lt;/a&gt; but I’ve started to regroup files and use a simple prefix in the key instead&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;stringsdict&lt;/em&gt;: this is used to handle plural&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;plist&lt;/em&gt;: I use this for the daily messages, so it’s a list of sentences, packaged by theme (Default, Premium, Star Wars, Xmas…)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;some assets&lt;/em&gt;: I only have some screenshots for the paywall that are translated (en/fr), I can default to English&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;txt&lt;/em&gt;: marketing texts &amp;amp; screenshots published on the App Store. This is downloaded into my app folder by fastlane.tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Localazy supports only the first 3 file formats: strings, stringsdict and plist, but that’s already better than some other solutions I considered.&lt;/p&gt;

&lt;p&gt;I suggested them to support txt files, you can &lt;a href="https://discuss.localazy.com/t/support-txt-files-would-allow-fastlane-metadata-support/159"&gt;vote for it&lt;/a&gt; if you like that idea.&lt;/p&gt;
&lt;h1&gt;
  
  
  Pricing
&lt;/h1&gt;

&lt;p&gt;Fair warning: you will probably have to pay. But something that feels fair.&lt;/p&gt;

&lt;p&gt;You get 200 phrases for free and can buy more. Adding 500 (so 700 total) will cost you a &lt;em&gt;one-time fee&lt;/em&gt; of $50, and 1000 (1200 total) $90.&lt;/p&gt;

&lt;p&gt;I found the project launch on Product Hunt and they advertised 1000 keys for free and a 75% discount. I contacted them about it and Václav was transparent about how this was too generous to build a viable business and that they had to decrease it. I wasn’t really surprised to be honest, and the current pricing still seems pretty reasonable to me.&lt;/p&gt;

&lt;p&gt;They also offer a starter pack subscription at $19/mo or $199/year, and it wouldn’t be surprising if they decided to drop the one time fee at some point. Hopefully that will include more affordable tiers, but in any case, Václav promised me that any phrases bought now will be owned forever, which is enough for me.&lt;/p&gt;

&lt;p&gt;You can get free phrases with a referral link (&lt;a href="https://localazy.com/register?ref=aAHPjuSXTN5m"&gt;here is mine&lt;/a&gt;, again, I have no shame). Know that this referral bonus is added on a daily basis, and you only get it if the user integrates their app (upload texts).&lt;br&gt;&lt;br&gt;
I was told they would offer more ways how to earn free phrases.&lt;/p&gt;
&lt;h1&gt;
  
  
  Setup
&lt;/h1&gt;

&lt;p&gt;The main pages you’ll want to look at are&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://localazy.com/docs/cli/installation#macos"&gt;Installation – Localazy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://localazy.com/docs/cli/quick-start-ios"&gt;Quick Start - iOS &amp;amp; macOS – Localazy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://localazy.com/docs/cli/ios-format"&gt;File Format - iOS / macOS – Localazy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It can be worth having a look at these two as well since they describe the configuration of the two core commands of Localazy: &lt;code&gt;upload&lt;/code&gt; and &lt;code&gt;download&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://localazy.com/docs/cli/upload-reference"&gt;Upload Reference – Localazy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://localazy.com/docs/cli/download-reference"&gt;Download Reference – Localazy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Install the CLI
&lt;/h2&gt;

&lt;p&gt;Installation is done via &lt;a href="https://brew.sh"&gt;Homebrew&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xcode-select --install # Only if you haven't done so before
brew tap localazy/tools
brew install localazy

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create your configuration file
&lt;/h2&gt;

&lt;p&gt;I followed the &lt;a href="https://localazy.com/docs/cli/quick-start-ios"&gt;Quick Start - iOS &amp;amp; macOS – Localazy&lt;/a&gt; instructions at first, but this didn’t work for me.&lt;/p&gt;

&lt;p&gt;I have a Watch Extension, so for instance I have a Base.lproj in both the “No Meat Today” subfolder and “No Meat Today Watch App Extension”.&lt;/p&gt;

&lt;p&gt;After playing with upload/download a bit, I ended up with this configuration.&lt;br&gt;
&lt;/p&gt;

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

    "writeKey": "my-write-key",
    "readKey": "my-read-key",

    "upload": {
        "files": [
            {
                "type": "ios-strings",
                "pattern": "No Meat Today/Base.lproj/Localizable.strings",
                "path": "No Meat Today"
            },
            {
                "type": "ios-strings",
                "pattern": "No Meat Today/fr.lproj/Localizable.strings",
                "path": "No Meat Today",
                "lang": "fr"
            },
            {
                "type": "ios-stringsdict",
                "pattern": "No Meat Today/Base.lproj/Localizable.stringsdict",
                "path": "No Meat Today"
            },
            {
                "type": "ios-stringsdict",
                "pattern": "No Meat Today/fr.lproj/Localizable.stringsdict",
                "path": "No Meat Today",
                "lang": "fr"
            },
            {
                "type": "ios-plist",
                "pattern": "No Meat Today/Base.lproj/Silliness.plist",
                "path": "No Meat Today"
            },
            {
                "type": "ios-plist",
                "pattern": "No Meat Today/fr.lproj/Silliness.plist",
                "path": "No Meat Today",
                "lang": "fr"
            },
            {
                "type": "ios-strings",
                "pattern": "No Meat Today Watch App Extension/en.lproj/Localizable.strings",
                "path": "No Meat Today Watch App Extension"
            },
            {
                "type": "ios-strings",
                "pattern": "No Meat Today Watch App Extension/fr.lproj/Localizable.strings",
                "path": "No Meat Today Watch App Extension",
                "lang": "fr"
            }
        ]
    },

    "download": {
        "files": "${path}/${iosLprojFolder}/${file}"
    }   
}

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

&lt;/div&gt;



&lt;p&gt;As you can see, unlike the proposed configuration I use a path variable so that I can support the same &lt;code&gt;Localizable.strings&lt;/code&gt;file in two different subfolders, which means I can translate all extensions (Watch App, Widget…).&lt;/p&gt;

&lt;p&gt;The app’s default language is English, and my app is localised in French, which means I already had translated texts. The above configuration allowed me to upload my translations in one go.&lt;/p&gt;

&lt;p&gt;Does it mean I’ll have to add new rows for each new language? I don’t think so.&lt;br&gt;&lt;br&gt;
If you edit a translation file and call &lt;code&gt;localazy upload&lt;/code&gt; it will push the edit translation for review, which is quite nice. While I will probably want to do that for French (since it’s my native language and I may spot typos or find better translations while coding), it’s unlikely that I will try to edit strings in a language that I don’t speak. So for these other languages, I will only download and not upload, since I won’t do any changes to these files.&lt;/p&gt;
&lt;h1&gt;
  
  
  Lingo: before you read the rest
&lt;/h1&gt;

&lt;p&gt;First, a reminder of what a localisation in a strings file looks like and some terms used thereafter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* This is a comment */
"a.key" = "This is a key";

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;translation notes: the comment / first line&lt;/li&gt;
&lt;li&gt;key: the left hand side of the 2nd line, a uniq string to identify your text across all languages&lt;/li&gt;
&lt;li&gt;phrase: the right hand side of the 2nd line&lt;/li&gt;
&lt;li&gt;source language: the language used in your Base.lproj files&lt;/li&gt;
&lt;li&gt;source phrases: the phrases in your source/base language&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Translating
&lt;/h1&gt;

&lt;p&gt;This is what the UI looks like. I think it’s pretty straightforward. But more importantly, I invited a translator who is not a dev and they found it easy to use as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8C_6ZqLB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-21.52.01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8C_6ZqLB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-21.52.01.png" alt="Translating your iOS app with Localazy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;everything that can help is there: the key, the source phrase, the translation notes, even the file path&lt;/li&gt;
&lt;li&gt;at the bottom you can see 2 types of suggestions: ShareTM is the database of translations from other apps/devs who decided to share their translations, and the other one is an automatic translation. This is really helpful to get a first quick &amp;amp; dirty translation of each phrase.&lt;/li&gt;
&lt;li&gt;I love bad puns&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Good to know &amp;amp; other tips
&lt;/h1&gt;

&lt;p&gt;Things below should help you get started, I figured some of it by trial and error, and the rest by talking with someone in the team (🙌 Vaclav!)&lt;/p&gt;

&lt;h2&gt;
  
  
  Your default language is kinda hidden
&lt;/h2&gt;

&lt;p&gt;When you first upload your strings, you can be surprised that you don’t find your base/source language. It took me a while to see it, but your source phrases are hidden behind the little sandwich menu next to the mention of your source language.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--H8EnQV-h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-21.13.04.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--H8EnQV-h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-21.13.04.png" alt="Translating your iOS app with Localazy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I first uploaded my strings, I only found the French language and couldn’t figure out where the English phrases were. I even tried to add en_US as a language before I realised the source language wasn’t treated as the other languages.&lt;/p&gt;

&lt;p&gt;I mentioned this to the team and apparently they’ll try to improve this.&lt;/p&gt;

&lt;p&gt;Know that another way to access your source phrases is by heading to the “File management” section.&lt;/p&gt;

&lt;h2&gt;
  
  
  You will lose control over the format of your files (but not the Base one 🙌, and it’s OK anyway)
&lt;/h2&gt;

&lt;p&gt;Up until now, I would make sure my files were organised in the same way, having the same number of lines, spaces, comments. I would then open both language files simultaneously and modify them at the same time.&lt;br&gt;&lt;br&gt;
The identical number of lines especially made it easier to spot discrepancies, which meant untranslated keys.&lt;/p&gt;

&lt;p&gt;So naturally, when I started searching for a service to handle translations, I imagined there might be one that would scan my files, keep the keys exactly where they were (append the missing ones) and only change the phrases/translations.&lt;/p&gt;

&lt;p&gt;But, this is not how things work.&lt;br&gt;&lt;br&gt;
Instead, &lt;em&gt;each time you download the translations, the language files are overwritten and keys are sorted alphabetically&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I can live with that, I guess. I mean, as I explained above, I’m unlikely to modify phrases in languages other than English and French.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting a key doesn’t delete it in the base language
&lt;/h2&gt;

&lt;p&gt;By default, base files are not overwritten, which means that if you delete a key, it will be deleted in all your language files except the base one, where you’ll have to delete it manually.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is a good thing, because it means that you can format your base file as you want.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In particular, you can make use of &lt;code&gt;// MARK : - Section&lt;/code&gt; to make it easier to navigate your Localizable.strings.&lt;br&gt;&lt;br&gt;
If you do use these, make sure you add a different comment (even an empty one such as &lt;code&gt;/**/&lt;/code&gt;) above the first key so that the MARK doesn’t show up as translation notes.&lt;/p&gt;

&lt;p&gt;Finally, if you do want to overwrite the files in your base language, you can for it by setting the includeSourceLang in your config file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if I change a translation in the source language?
&lt;/h2&gt;

&lt;p&gt;This is something I was worried about and it works just as you would expect: when you edit a translation, all languages see a notice that the source phrase was changed, so that they can be changed if necessary.&lt;/p&gt;

&lt;p&gt;If you do it from the web, you get a bit more flexibility because you can decide whether existing translations need to be updated or not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zCgNF_1C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-22.11.02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zCgNF_1C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-22.11.02.png" alt="Translating your iOS app with Localazy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But, I wouldn’t do it from there, because, remember, the source phrases are not downloaded, so you’d have to change it manually in Xcode.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if I change a translation in a language that is not the source language?
&lt;/h2&gt;

&lt;p&gt;If you change a translation in Xcode, say in the fr.lproj/Localizable.strings, and then run &lt;code&gt;localazy upload&lt;/code&gt;, it will upload the translation for review but it won’t be taken into account until your review it&lt;/p&gt;

&lt;p&gt;This means that if you run &lt;code&gt;localazy download&lt;/code&gt; before you review the change on the web, your change will be overwritten.&lt;/p&gt;

&lt;p&gt;But what if you run &lt;code&gt;localazy upload&lt;/code&gt; again before you review it, will you lose your change? No, because there is a versioning system and you’ll still be able to find your unreviewed change. 💪&lt;/p&gt;

&lt;h2&gt;
  
  
  There is a Glossary
&lt;/h2&gt;

&lt;p&gt;You can add terms that require extra context or attention in a Glossary.&lt;/p&gt;

&lt;p&gt;For instance, I invented the term “Cowliday” and added it to the Glossary to explain it and give some instructions that will show up each time the term is used.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VqOopNUM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-22.03.29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VqOopNUM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-22.03.29.png" alt="Translating your iOS app with Localazy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When translating a phrase, the term is highlighted and hovering it will show the comments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WaHZLKLg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-22.05.42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WaHZLKLg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/content/images/2021/05/CleanShot-2021-05-28-at-22.05.42.png" alt="Translating your iOS app with Localazy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Parting notes
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Localazy supports all the file formats I expected it to, and hopefully txt support will arrive some day to simplify App Store marketing localisation&lt;/li&gt;
&lt;li&gt;It’s free for apps with less than 200 phrases, and affordable for small apps plus there are ways to get free phrases&lt;/li&gt;
&lt;li&gt;CLI is a charm&lt;/li&gt;
&lt;li&gt;Setup is pretty easy if you follow the configuration above&lt;/li&gt;
&lt;li&gt;The UI to translate is reactive and works well, and is easy to use for non-devs (well, I tested with a single person for now)&lt;/li&gt;
&lt;li&gt;Register using &lt;a href="https://localazy.com/register?ref=aAHPjuSXTN5m"&gt;my referral link&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Final note: if you want to help translate &lt;a href="https://nomeat.today"&gt;No Meat Today&lt;/a&gt; in your language, I’m looking for volunteers.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>tools</category>
    </item>
    <item>
      <title>Capybara: 2 ways to click links in emails</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Sat, 19 Dec 2020 19:02:00 +0000</pubDate>
      <link>https://dev.to/sowenjub/capybara-2-ways-to-click-links-in-emails-44hg</link>
      <guid>https://dev.to/sowenjub/capybara-2-ways-to-click-links-in-emails-44hg</guid>
      <description>&lt;p&gt;If you want to write system tests you'll probably want to cover a use-case involving an email sent to a user (for instance, a sign up confirmation link).&lt;/p&gt;

&lt;p&gt;There are at least 2 ways to do that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;with &lt;a href="https://github.com/sparklemotion/nokogiri"&gt;nokogiri&lt;/a&gt; if you want to keep things lean&lt;/li&gt;
&lt;li&gt;with &lt;a href="https://github.com/DavyJonesLocker/capybara-email"&gt;capybara-email&lt;/a&gt; if you appreciate a little comfort&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  With nokogiri
&lt;/h1&gt;

&lt;p&gt;Add the gem to your &lt;code&gt;Gemfile&lt;/code&gt; and run &lt;code&gt;bundle install&lt;/code&gt;.&lt;br&gt;
Chances are you'll find it in your Gemfile.lock already since it's a pretty standard gem with 5k+ stars.&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;group&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"nokogiri"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you can open your email and click the line like this:&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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ActionMailer&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;deliveries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;
&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Nokogiri&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;(&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;html_part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;target_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"a:contains('Click this link')"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;visit&lt;/span&gt; &lt;span class="n"&gt;target_link&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"href"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  With capybara-email gem
&lt;/h1&gt;

&lt;p&gt;Add the gem to your &lt;code&gt;Gemfile&lt;/code&gt; and run &lt;code&gt;bundle install&lt;/code&gt;. It only has 300+ stars, which is still pretty good, and it's also a rather simple gem so you could easily decide to contribute to it if your app depended on it.&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;group&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"capybara-email"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you can open your email with just 2 lines:&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;open_email&lt;/span&gt; &lt;span class="s2"&gt;"arnaud@example.com"&lt;/span&gt;
&lt;span class="n"&gt;current_email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click_link&lt;/span&gt; &lt;span class="s2"&gt;"Click this link"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>rails</category>
      <category>capybara</category>
      <category>testing</category>
      <category>howto</category>
    </item>
    <item>
      <title>Replacing Selenium with Cuprite for Rails system tests</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Fri, 18 Dec 2020 12:02:00 +0000</pubDate>
      <link>https://dev.to/sowenjub/replacing-selenium-with-cuprite-for-rails-system-tests-3oib</link>
      <guid>https://dev.to/sowenjub/replacing-selenium-with-cuprite-for-rails-system-tests-3oib</guid>
      <description>&lt;h1&gt;
  
  
  Replacing Selenium with Cuprite for Rails system tests
&lt;/h1&gt;

&lt;p&gt;In the &lt;a href="https://guides.rubyonrails.org/testing.html"&gt;world of tests&lt;/a&gt;, system tests are as close to real users as it gets: you control a browser and make it use your app as you expect humans to.&lt;/p&gt;

&lt;p&gt;To do so, you need two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;a browser&lt;/strong&gt; : you will see it (safari, chrome…) run on its own on your machine unless you set it to be "headless", a gory way to tell your machine that you actually don't want to see the insides of your tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a driver&lt;/strong&gt; : the pilot that will take your test instructions and command the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, Rails ships with Selenium, but there's a new kid in town: &lt;a href="https://github.com/rubycdp/cuprite"&gt;Cuprite&lt;/a&gt;, a "&lt;em&gt;Headless Chrome driver for Capybara&lt;/em&gt;", which is faster and has some nice tricks up its sleeves like options to pause and debug.&lt;/p&gt;

&lt;p&gt;I discovered cuprite thanks to Evil Martians blog post: "**&lt;a href="https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing"&gt;System of a test:** Proper browser testing in Ruby on Rails&lt;/a&gt;".&lt;/p&gt;

&lt;p&gt;The following is just a quick-start version for those who don't care about docker or rspec, with some minor twists.&lt;/p&gt;

&lt;h1&gt;
  
  
  Basic setup
&lt;/h1&gt;

&lt;p&gt;To get started, you simply need to add the cuprite gem. Selenium will have to stay unless you're &lt;a href="https://github.com/rails/rails/pull/39179"&gt;on Rails 6.1&lt;/a&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="n"&gt;group&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'capybara'&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'selenium-webdriver'&lt;/span&gt; &lt;span class="c1"&gt;# Only for rails &amp;lt;= 6.1&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'cuprite'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And then you can edit &lt;code&gt;test/application_system_test_case.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="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"test_helper"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"capybara/cuprite"&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;- Add this&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationSystemTestCase&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionDispatch&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SystemTestCase&lt;/span&gt;
  &lt;span class="c1"&gt;# And replace selenium with cuprite&lt;/span&gt;
  &lt;span class="n"&gt;driven_by&lt;/span&gt; &lt;span class="ss"&gt;:cuprite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: :chromium&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;screen_size: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="p"&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;And with this you're good to go.&lt;/p&gt;

&lt;p&gt;You'll notice that I'm not using &lt;code&gt;:chrome&lt;/code&gt; but the open source browser it's based on: &lt;a href="https://www.chromium.org/getting-involved/download-chromium"&gt;Chromium&lt;/a&gt;. This is because I just uninstalled Chrome, because &lt;a href="https://chromeisbad.com"&gt;https://chromeisbad.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/sowenjub/status/1338958371844726784?s=20"&gt;https://twitter.com/sowenjub/status/1338958371844726784?s=20&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  A more elaborate setup
&lt;/h1&gt;

&lt;p&gt;There are a couple more setup files I grabbed from &lt;a href="https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing"&gt;Evil Martians' setup&lt;/a&gt;, with a slightly different organisation since I'm not using rspec.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test/
  test_helpers/
    system/
      better_rails_system_tests.rb
      capybara_setup.rb
      cuprite_helpers.rb
      cuprite_setup.rb

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

&lt;/div&gt;



&lt;p&gt;And with this, &lt;code&gt;test/application_system_test_case.rb&lt;/code&gt; will end up looking like this:&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="s2"&gt;"test_helper"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"test_helpers/system/better_rails_system_tests"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"test_helpers/system/capybara_setup"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"test_helpers/system/cuprite_helpers"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"test_helpers/system/cuprite_setup"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationSystemTestCase&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionDispatch&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SystemTestCase&lt;/span&gt;
  &lt;span class="n"&gt;driven_by&lt;/span&gt; &lt;span class="ss"&gt;:cuprite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: :chromium&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;screen_size: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;BetterRailsSystemTests&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CupriteHelpers&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And of course you need to create the files, taken from Evil Martians.&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;# test_helpers/system/better_rails_system_tests&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;BetterRailsSystemTests&lt;/span&gt;
  &lt;span class="c1"&gt;# Use our `Capybara.save_path` to store screenshots with other capybara artifacts&lt;/span&gt;
  &lt;span class="c1"&gt;# (Rails screenshots path is not configurable https://github.com/rails/rails/blob/49baf092439fc74fc3377b12e3334c3dd9d0752f/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L79)&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;absolute_image_path&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&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="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/screenshots/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;image_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png"&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;# Make failure screenshots compatible with multi-session setup.&lt;/span&gt;
  &lt;span class="c1"&gt;# That's where we use Capybara.last_used_session introduced before.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;take_screenshot&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_used_session&lt;/span&gt;

    &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;using_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_used_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;super&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;# test_helpers/system/capybara_setup&lt;/span&gt;

&lt;span class="c1"&gt;# Usually, especially when using Selenium, developers tend to increase the max wait time.&lt;/span&gt;
&lt;span class="c1"&gt;# With Cuprite, there is no need for that.&lt;/span&gt;
&lt;span class="c1"&gt;# We use a Capybara default value here explicitly.&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_max_wait_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="c1"&gt;# Normalize whitespaces when using `has_text?` and similar matchers,&lt;/span&gt;
&lt;span class="c1"&gt;# i.e., ignore newlines, trailing spaces, etc.&lt;/span&gt;
&lt;span class="c1"&gt;# That makes tests less dependent on slightly UI changes.&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_normalize_ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# Where to store system tests artifacts (e.g. screenshots, downloaded files, etc.).&lt;/span&gt;
&lt;span class="c1"&gt;# It could be useful to be able to configure this path from the outside (e.g., on CI).&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_path&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CAPYBARA_ARTIFACTS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"./tmp/capybara"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The Capybara.using_session allows you to manipulate a different browser session, and thus, multiple independent sessions within a single test scenario. That’s especially useful for testing real-time features, e.g., something with WebSocket.&lt;/span&gt;
&lt;span class="c1"&gt;# This patch tracks the name of the last session used. We’re going to use this information to support taking failure screenshots in multi-session tests.&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;singleton_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Module&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;do&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:last_used_session&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;using_session&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_used_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;
  &lt;span class="k"&gt;ensure&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_used_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;For this one I created a dedicated file but you could put it at the end of &lt;code&gt;cuprite_setup&lt;/code&gt; as well.&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;# test_helpers/system/cuprite_helpers&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;CupriteHelpers&lt;/span&gt;
  &lt;span class="c1"&gt;# Drop #pause anywhere in a test to stop the execution.&lt;/span&gt;
  &lt;span class="c1"&gt;# Useful when you want to checkout the contents of a web page in the middle of a test&lt;/span&gt;
  &lt;span class="c1"&gt;# running in a headful mode.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pause&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Drop #debug anywhere in a test to open a Chrome inspector and pause the execution&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;debug&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="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&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="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;



&lt;span class="c1"&gt;# test_helpers/system/cuprite_setup&lt;/span&gt;

&lt;span class="c1"&gt;# First, load Cuprite Capybara integration&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"capybara/cuprite"&lt;/span&gt;

&lt;span class="c1"&gt;# Then, we need to register our driver to be able to use it later&lt;/span&gt;
&lt;span class="c1"&gt;# with #driven_by method.&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:cuprite&lt;/span&gt;&lt;span class="p"&gt;)&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;app&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cuprite&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Driver&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;app&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;window_size: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="c1"&gt;# See additional options for Dockerized environment in the respective section of this article&lt;/span&gt;
      &lt;span class="ss"&gt;browser_options: &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
      &lt;span class="c1"&gt;# Increase Chrome startup wait time (required for stable CI builds)&lt;/span&gt;
      &lt;span class="ss"&gt;process_timeout: &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;# Enable debugging capabilities&lt;/span&gt;
      &lt;span class="ss"&gt;inspector: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;# Allow running Chrome in a headful mode by setting HEADLESS env&lt;/span&gt;
      &lt;span class="c1"&gt;# var to a falsey value&lt;/span&gt;
      &lt;span class="ss"&gt;headless: &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="s2"&gt;"HEADLESS"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;in?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sx"&gt;%w[n 0 no false]&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="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Configure Capybara to use :cuprite driver by default&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;javascript_driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:cuprite&lt;/span&gt; 

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

&lt;/div&gt;



&lt;p&gt;And now we just have to write a simple test:&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="s2"&gt;"application_system_test_case"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationSystemTestCase&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"open home screen"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;visit&lt;/span&gt; &lt;span class="n"&gt;root_url&lt;/span&gt;
    &lt;span class="n"&gt;save_and_open_page&lt;/span&gt; &lt;span class="c1"&gt;# Will save the html page in /tmp/capybara and open it in your default browser&lt;/span&gt;
    &lt;span class="n"&gt;take_screenshot&lt;/span&gt; &lt;span class="c1"&gt;# Will save a screenshot in /tmp/capybara/screenshots&lt;/span&gt;
    &lt;span class="n"&gt;pause&lt;/span&gt; &lt;span class="c1"&gt;# To see the current view, requires HEADLESS=0 (or n, no, false)&lt;/span&gt;
    &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# To see the current view with debug tools&lt;/span&gt;
    &lt;span class="n"&gt;assert_selector&lt;/span&gt; &lt;span class="s2"&gt;"h1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="s2"&gt;"Hello, world"&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;To launch it, run the following:&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;# If you want to see the browser or use `pause`&lt;/span&gt;
&lt;span class="nv"&gt;HEADLESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 rails &lt;span class="nb"&gt;test test&lt;/span&gt;/system/home_test.rb

&lt;span class="c"&gt;# Otherwise this will do&lt;/span&gt;
rails &lt;span class="nb"&gt;test test&lt;/span&gt;/system/home_test.rb

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

&lt;/div&gt;



</description>
      <category>rails</category>
      <category>testing</category>
      <category>howto</category>
    </item>
    <item>
      <title>Auto-linking URLs with Trix Editor and StimulusJS</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Tue, 20 Oct 2020 08:25:00 +0000</pubDate>
      <link>https://dev.to/sowenjub/auto-linking-urls-with-trix-editor-and-stimulusjs-4gh8</link>
      <guid>https://dev.to/sowenjub/auto-linking-urls-with-trix-editor-and-stimulusjs-4gh8</guid>
      <description>&lt;p&gt;The &lt;a href="https://trix-editor.org/"&gt;https://trix-editor.org&lt;/a&gt; is fantastic, but there's one simple thing I expected to be built-in that isn't: copy/pasting a URL should create a link automatically instead of inserting the link as plain text.&lt;/p&gt;

&lt;p&gt;Thinking of it, it's not so surprising because there are many things one could want to do instead, like embedding a video or a tweet. But it's a &lt;a href="https://github.com/basecamp/trix/issues/167"&gt;common request&lt;/a&gt;, and the GitHub issue includes a solution with jQuery that I used to build my Stimulus controller.&lt;/p&gt;

&lt;p&gt;So, without further ado, here's a simple 2 steps rails tutorial to add URL auto-linking to your forms.&lt;/p&gt;

&lt;h1&gt;
  
  
  The controller
&lt;/h1&gt;

&lt;p&gt;Create a &lt;code&gt;/app/javascript/controller/trix_paste_controller.js&lt;/code&gt; file and copy/paste the code below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;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="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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paste&lt;/span&gt;&lt;span class="dl"&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;pasteHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bind&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paste&lt;/span&gt;&lt;span class="dl"&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;pasteHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bind&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;pasteHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;pastedText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboardData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;getData&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&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;pastedText&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;pastedText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/(?:&lt;/span&gt;&lt;span class="sr"&gt;www&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;(?!&lt;/span&gt;&lt;span class="sr"&gt;www&lt;/span&gt;&lt;span class="se"&gt;))[^\s\.]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.[^\s]{2,}&lt;/span&gt;&lt;span class="sr"&gt;|www&lt;/span&gt;&lt;span class="se"&gt;\.[^\s]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.[^\s]{2,})&lt;/span&gt;&lt;span class="sr"&gt;$/ig&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;pasteUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pastedText&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;pasteUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pastedText&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;editor&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;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getDocument&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentSelection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getSelectedRange&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;textWeAreInterestedIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentSelection&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;startOfPastedText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textWeAreInterestedIn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pastedText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordUndoEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Auto Link Paste&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setSelectedRange&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;startOfPastedText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentSelection&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
    &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activateAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pastedText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setSelectedRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentSelection&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;h1&gt;
  
  
  Your forms
&lt;/h1&gt;

&lt;p&gt;And now, anytime you want need this auto-linking feature, just reference the controller like so:&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="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;rich_text_area&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;controller: &lt;/span&gt;&lt;span class="s2"&gt;"trix-paste"&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, and it's "trix-paste" and not "trix-autolink" because I have further plans for this controller.&lt;/p&gt;




&lt;p&gt;That's it! Follow me here or &lt;a href="https://twitter.com/sowenjub"&gt;@sowenjub&lt;/a&gt; on twitter for more&lt;/p&gt;

</description>
      <category>rails</category>
      <category>stimulus</category>
      <category>trix</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Multiple YAML defaults in Rails fixtures</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Tue, 25 Aug 2020 16:05:00 +0000</pubDate>
      <link>https://dev.to/sowenjub/multiple-yaml-defaults-in-rails-fixtures-4j3d</link>
      <guid>https://dev.to/sowenjub/multiple-yaml-defaults-in-rails-fixtures-4j3d</guid>
      <description>&lt;p&gt;You know that you can use &lt;a href="https://api.rubyonrails.org/v3.1/classes/ActiveRecord/Fixtures.html"&gt;YAML defaults in your fixtures&lt;/a&gt; to extract common attributes values and make your fixtures more readable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;DEFAULTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;DEFAULTS&lt;/span&gt;
  &lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= 3.weeks.ago.to_s(:db) %&amp;gt;&lt;/span&gt;

&lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Smurf&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*DEFAULTS&lt;/span&gt;

&lt;span class="na"&gt;second&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Fraggle&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*DEFAULTS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What if you want to use more than one set of defaults? &lt;/p&gt;

&lt;h1&gt;
  
  
  The old way - DEFAULTS
&lt;/h1&gt;

&lt;p&gt;The Rails example is a bit confusing because it uses "DEFAULTS" for both the label and the anchor/alias, but here's how to handle multiple defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;DEFAULTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;decisions&lt;/span&gt;
  &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🧠&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Decisions&lt;/span&gt;

&lt;span class="na"&gt;DEFAULTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;help&lt;/span&gt;
  &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;👋&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Help&lt;/span&gt;

&lt;span class="na"&gt;founders_decisions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;founders&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*decisions&lt;/span&gt;

&lt;span class="na"&gt;founders_help_requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;founders&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To understand how this works, you need to know two things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;a href="https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html#class-ActiveRecord::FixtureSet-label-Support+for+YAML+defaults"&gt;Rails docs&lt;/a&gt; tell us that it doesn't matter how many "DEFAULTS" label we have: they won't overwrite themselves even though they use the same label.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Any fixture labeled “DEFAULTS” is safely ignored&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;a href="https://yaml.org/spec/1.2/spec.html"&gt;YAML spec&lt;/a&gt; explains how aliasing works&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Repeated nodes (objects) are first identified by an anchor (marked with the ampersand - “&amp;amp;”), and are then aliased (referenced with an asterisk - “*”) thereafter.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Edge Rails way - fixture:ignore
&lt;/h1&gt;

&lt;p&gt;There's another way to do that on &lt;a href="https://edgeapi.rubyonrails.org/classes/ActiveRecord/FixtureSet.html#class-ActiveRecord::FixtureSet-label-Support+for+YAML+defaults"&gt;Edge Rails&lt;/a&gt;. You can specify fixtures that should be ignored and that you can therefore use as defaults.&lt;/p&gt;

&lt;p&gt;We can rewrite the example above like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;_fixture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;decisions&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;help&lt;/span&gt;

&lt;span class="na"&gt;decisions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;decisions&lt;/span&gt;
  &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🧠&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Decisions&lt;/span&gt;

&lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;help&lt;/span&gt;
  &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;👋&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Help&lt;/span&gt;

&lt;span class="na"&gt;founders_decisions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;founders&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*decisions&lt;/span&gt;

&lt;span class="na"&gt;founders_help_requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;founders&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Which one should I use?
&lt;/h1&gt;

&lt;p&gt;If you're using the Rails 6.0.3.2 or below, you don't have a choice anyway. But even then, while the edge option is interesting because the anchor/alias mirrors the label, I'll stick with the "old way" for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;it's less code (you don't have to write the _fixture block)&lt;/li&gt;
&lt;li&gt;the DEFAULTS label is easy to spot and explicit. You could use &lt;code&gt;DECISION_DEFAULTS: &amp;amp;decisions&lt;/code&gt; instead of &lt;code&gt;decisions: &amp;amp;decisions&lt;/code&gt; but the label isn't really useful and, again, you're writing more code than if it's just &lt;code&gt;DEFAULTS: &amp;amp;decisions&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>rails</category>
      <category>testing</category>
      <category>tips</category>
      <category>howto</category>
    </item>
    <item>
      <title>Power your Publish (Swift) static site with Tailwind CSS</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Thu, 13 Aug 2020 13:42:00 +0000</pubDate>
      <link>https://dev.to/sowenjub/power-your-publish-swift-static-site-with-tailwind-css-3hj4</link>
      <guid>https://dev.to/sowenjub/power-your-publish-swift-static-site-with-tailwind-css-3hj4</guid>
      <description>&lt;p&gt;Within minutes, you will be able to deploy a static site/blog (with &lt;a href="https://www.notion.so/sowenjub/Power-your-Publish-Swift-static-site-with-TailwindCSS-58cac4f32b194a949b8c6a5050e6c3bd#feea835bb98c48429bd916820f17c355"&gt;Publish&lt;/a&gt;) that you can style easily (with &lt;a href="https://tailwindcss.com"&gt;TailwindCSS&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If you don't want to create your own theme, you can save some time and use my &lt;a href="//../casperish"&gt;Casperish theme&lt;/a&gt;, a port of &lt;a href="https://ghost.org"&gt;Ghost&lt;/a&gt;'s Casper theme using Tailwind CSS.&lt;/p&gt;

&lt;h1&gt;
  
  
  Install Publish
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/JohnSundell/Publish"&gt;Publish&lt;/a&gt; is a static site generator built specifically for Swift developers, created by John Sundell (&lt;a href="https://twitter.com/johnsundell"&gt;@johnsundell&lt;/a&gt;) and powering his very own &lt;a href="http://swiftbysundell.com/"&gt;swiftbysundell.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You need to have installed Xcode and its Command Line Tools. It's preferable to have a basic understanding of how it works (build, run, understand crash logs).&lt;/p&gt;

&lt;p&gt;For now, open your terminal and follow the &lt;a href="https://github.com/JohnSundell/Publish#quick-start"&gt;README&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
git clone https://github.com/JohnSundell/Publish.git
cd Publish
make

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

&lt;/div&gt;



&lt;h1&gt;
  
  
  Create your website
&lt;/h1&gt;

&lt;p&gt;Publish is just the engine, it's now time to create your website.&lt;/p&gt;

&lt;p&gt;Let's get out of /Publish and create your website source next to it. We'll call it &lt;code&gt;MyWebsite&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
cd ..
mkdir MyWebsite
cd MyWebsite
publish new

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

&lt;/div&gt;



&lt;p&gt;And since we're impatient, let's see what it looks like at this point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
publish run

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

&lt;/div&gt;



&lt;p&gt;This should output something like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Fetching https://github.com/johnsundell/publish.git
…
…
…
Publishing MyWebsite (6 steps)
[1/6] Copy 'Resources' files
[2/6] Add Markdown files from 'Content' folder
[3/6] Sort items
[4/6] Generate HTML
[5/6] Generate RSS feed
[6/6] Generate site map
✅ Successfully published MyWebsite
🌍 Starting web server at http://localhost:8000

Press ENTER to stop the server and exit

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

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:8000/"&gt;http://localhost:8000&lt;/a&gt; in your browser of choice (Safari here) and it should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rZT_Bxva--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-02_at_14.52.43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rZT_Bxva--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-02_at_14.52.43.png" alt="/images/3/Screenshot&amp;lt;em&amp;gt;2020-08-02&amp;lt;/em&amp;gt;at_14.52.43.png" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;or in dark mode:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Vcdc3FQh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-01_at_23.18.49.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Vcdc3FQh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-01_at_23.18.49.png" alt="/images/3/Screenshot&amp;lt;em&amp;gt;2020-08-01&amp;lt;/em&amp;gt;at_23.18.49.png" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Time to launch Xcode
&lt;/h2&gt;

&lt;p&gt;We've been lazy alright, it's time to open Xcode and dive into some code.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;/Publish/MyWebsite&lt;/code&gt; run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
open Package.swift

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

&lt;/div&gt;



&lt;p&gt;It will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Sly52j1a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-01_at_23.40.21.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Sly52j1a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-01_at_23.40.21.png" alt="/images/3/Screenshot&amp;lt;em&amp;gt;2020-08-01&amp;lt;/em&amp;gt;at_23.40.21.png" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A couple things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can see at the top of the screen that the active scheme is "My Mac". This is important because you won't be able to build/run on devices or simulators&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/Sources/MyWebsite/main.swift&lt;/code&gt; is were you can configure your app&lt;/li&gt;
&lt;li&gt;You won't have the &lt;code&gt;package.json&lt;/code&gt; or &lt;code&gt;yarn.lock&lt;/code&gt;files at this point&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/Content&lt;/code&gt; contains the markdown content we saw when we opened the site (ex: "Welcome to MyWebsite")&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pause for coffee and think about your future
&lt;/h2&gt;

&lt;p&gt;From now on, before your refresh your browser to see the result of any change you made to your configuration or content, don't forget to rebuild your site in Xcode using ⌘+R.&lt;/p&gt;

&lt;h1&gt;
  
  
  Getting ready for TailwindCSS with a new Plot theme
&lt;/h1&gt;

&lt;p&gt;To build the website from the Content folder, Publish used its default theme called Foundation which is two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/JohnSundell/Publish/blob/master/Sources/Publish/API/Theme%2BFoundation.swift"&gt;Theme+Foundation.swift&lt;/a&gt; which adopts the &lt;a href="https://github.com/JohnSundell/Publish/blob/master/Sources/Publish/API/HTMLFactory.swift"&gt;HTMLFactory&lt;/a&gt; protocol in order to &lt;em&gt;create HTML for a site's various locations using the Plot DSL&lt;/em&gt;. Basically, it defines the HTML markup of each pages (index, article, page, tags index, tag page) ⇒ &lt;em&gt;we'll want to have access to this in some way to inject our TailwindCSS classes.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/JohnSundell/Publish/blob/master/Resources/FoundationTheme/styles.css"&gt;styles.css&lt;/a&gt; which is the CSS stylesheet for that theme ⇒ &lt;em&gt;we'll want to change that with the ones generated by TailwindCSS&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you may have guessed, we need to build our own theme.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do we need to do?
&lt;/h2&gt;

&lt;p&gt;Let's start with the end and work our way up.&lt;/p&gt;

&lt;p&gt;We will tell Publish to use our own theme, so open &lt;code&gt;main.swift&lt;/code&gt; and change the last line (should be line 25).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// main.swift

// From
try MyWebsite().publish(withTheme: .foundation)
// to
try MyWebsite().publish(withTheme: .myTheme)

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

&lt;/div&gt;



&lt;p&gt;Alright. Except this won't work of course so don't try to build or run right now.&lt;/p&gt;

&lt;p&gt;Now we need to define &lt;code&gt;myTheme&lt;/code&gt; somewhere. We'll just create a new theme file in the Sources, next to &lt;code&gt;/Sources/MyWebsite/main.swift&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's call it &lt;code&gt;Theme+MyTheme.swift&lt;/code&gt; (from Xcode, right click on the &lt;code&gt;Sources/MyWebsite&lt;/code&gt; folder and "New file").&lt;/p&gt;

&lt;p&gt;But what should we put in there? The &lt;a href="https://github.com/JohnSundell/Publish#building-an-html-theme"&gt;Publish README&lt;/a&gt; explains how to build a custom theme. Again, we'll write things as they should be in the end, and work our way up.&lt;/p&gt;

&lt;p&gt;So we want to end up with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import Foundation
import Plot
import Publish

extension Theme where Site == MyWebsite {
    static var myTheme: Self {
        Theme(
            htmlFactory: MyThemeHTMLFactory&amp;lt;MyWebsite&amp;gt;(),
            resourcePaths: ["Resources/MyTheme/styles.css"]
        )
    }
}

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

&lt;/div&gt;



&lt;p&gt;What did we add there that doesn't exist already? Two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MyThemeHTMLFactory&lt;/li&gt;
&lt;li&gt;"Resources/MyTheme/styles.css"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looks familiar? This is the same setup I mentioned above when I introduced the Foundation theme.&lt;/p&gt;

&lt;p&gt;If you open the &lt;code&gt;Theme+Foundation.swift&lt;/code&gt; file (you can find it in Xcode under "Swift Package Dependencies" by opening the Publish package and going down to &lt;code&gt;/Publish/Sources/Publish/API&lt;/code&gt;), you will&lt;/p&gt;

&lt;h1&gt;
  
  
  MyThemeHTMLFactory: Our own HTML Factory
&lt;/h1&gt;

&lt;p&gt;We want to build a custom theme, so let's take inspiration from the free one we're given.&lt;/p&gt;

&lt;p&gt;In Xcode, under "Swift Package Dependencies" you will find the Publish package, and inside of it is &lt;code&gt;Publish/Sources/Publish/API/Theme+Foundation.swift&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few lines below the &lt;code&gt;public extension Theme&lt;/code&gt; declaration that we don't need since we just defined our own, you will find the foundation factory that we will use as a starting point.&lt;/p&gt;

&lt;p&gt;We need to do 3 things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the rest of the file, which includes one struct and one extension
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
private struct FoundationHTMLFactory&amp;lt;Site: Website&amp;gt;: HTMLFactory {
  …
}

private extension Node where Context == HTML.BodyContext {
  …
}

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Paste it in our &lt;code&gt;Theme+MyTheme.swift&lt;/code&gt;, below our &lt;code&gt;extension Theme where Site == MyWebsite {}&lt;/code&gt; declaration.&lt;/li&gt;
&lt;li&gt;Change &lt;code&gt;FoundationHTMLFactory&lt;/code&gt; to &lt;code&gt;MyThemeHTMLFactory&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So to be clear, &lt;code&gt;Theme+MyTheme.swift&lt;/code&gt; should look 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;
import Foundation
import Plot
import Publish

extension Theme where Site == MyWebsite {
    static var myTheme: Self {
        Theme(
            htmlFactory: MyThemeHTMLFactory&amp;lt;MyWebsite&amp;gt;(),
            resourcePaths: ["Resources/MyTheme/styles.css"]
        )
    }
}

private struct MyThemeHTMLFactory&amp;lt;Site: Website&amp;gt;: HTMLFactory {
        // What's inside didn't change
}

private extension Node where Context == HTML.BodyContext {
        // What's inside didn't change
}

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

&lt;/div&gt;



&lt;p&gt;OK!&lt;/p&gt;

&lt;p&gt;There's one last thing we need: "Resources/MyTheme/styles.css"&lt;/p&gt;

&lt;h1&gt;
  
  
  Let's add TailwindCSS!
&lt;/h1&gt;

&lt;p&gt;Let's add Tailwind by following the documentation &lt;a href="https://tailwindcss.com/docs/installation"&gt;https://tailwindcss.com/docs/installation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We will also add the typography plugin since we're dealing with Markdown, and this plugin was specifically created to &lt;em&gt;add beautiful typographic defaults to any vanilla HTML you don't control&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;/MyWebsite&lt;/code&gt;, open your terminal and run:&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 tailwindcss
yarn add @tailwindcss/typography

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

&lt;/div&gt;



&lt;p&gt;We'll now add our theme stylesheet in &lt;code&gt;Resources&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
cd Resources
mkdir MyTheme
touch theme.css

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

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;theme.css&lt;/code&gt; in Xcode, and add those lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
@import "tailwindcss/base";

@import "tailwindcss/components";

@import "tailwindcss/utilities";

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

&lt;/div&gt;



&lt;p&gt;We need to create a config file for TailwindCSS since we want the typography plugin, so we'll create a config file in &lt;code&gt;/Resources/MyTheme&lt;/code&gt; as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
npx tailwindcss init

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

&lt;/div&gt;



&lt;p&gt;Open the &lt;code&gt;tailwind.config.js&lt;/code&gt; file it created in Xcode and add the typography plugin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
module.exports = {
  content: [],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [require('@tailwindcss/typography')],
}

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

&lt;/div&gt;



&lt;p&gt;Now to give this a spin and make sure it works so far, let's use the tailwind cli (from within &lt;code&gt;/Resources/MyTheme&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
npx tailwindcss build theme.css -o styles.css -c tailwind.config.js

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

&lt;/div&gt;



&lt;p&gt;TADA! We now have our .css file&lt;/p&gt;

&lt;p&gt;At this point (or anytime you make a change), you can just run the package &lt;code&gt;Product &amp;gt; Run&lt;/code&gt; (or ⌘+R) and refresh your page in the browser (assuming your server, launched with &lt;code&gt;publish run&lt;/code&gt; is still running).&lt;/p&gt;

&lt;p&gt;It should look like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nTg_D_SF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-02_at_22.52.48.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nTg_D_SF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-02_at_22.52.48.png" alt="/images/3/Screenshot&amp;lt;em&amp;gt;2020-08-02&amp;lt;/em&amp;gt;at_22.52.48.png" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, that's a step back for sure.&lt;/p&gt;

&lt;p&gt;But now, you can ride like the wind and build your own theme using Tailwind!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;We already know it works since all styles were reset, but let's add a class to make sure it really does.&lt;/p&gt;

&lt;p&gt;Open your &lt;code&gt;Theme+MyTheme.swift&lt;/code&gt; file, locate &lt;code&gt;makeIndexHTML()&lt;/code&gt; and change the header as explained below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
func makeIndexHTML(for index: Index,
                       context: PublishingContext&amp;lt;Site&amp;gt;) throws -&amp;gt; HTML {
        HTML(
            .lang(context.site.language),
            .head(for: index, on: context.site),
            .body(
                .header(for: context, selectedSection: nil),
                .wrapper(
                    // Turn this
                    .h1(.text(index.title)),
                    // into this
                    .h1(.text(index.title), .class("text-2xl font-semibold")),
                    .p(
                        .class("description"),
                        .text(context.site.description)
                    ),
                    .h2("Latest content"),
                    .itemList(
                        for: context.allItems(
                            sortedBy: \.date,
                            order: .descending
                        ),
                        on: context.site
                    )
                ),
                .footer(for: context.site)
            )
        )
    }

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

&lt;/div&gt;



&lt;p&gt;Now rerun (⌘+R), refresh your browser, and enjoy a bigger title!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WyVus5oq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-02_at_22.58.05.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WyVus5oq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-02_at_22.58.05.png" alt="/images/3/Screenshot&amp;lt;em&amp;gt;2020-08-02&amp;lt;/em&amp;gt;at_22.58.05.png" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's not much, but there rest is up to you, sky's the limit.&lt;/p&gt;

&lt;h1&gt;
  
  
  Controlling styles.css File Size
&lt;/h1&gt;

&lt;p&gt;Right now we included all TailwindCSS styles in the &lt;code&gt;styles.css&lt;/code&gt; file, so the file is 10 times bigger than it could be.&lt;/p&gt;

&lt;p&gt;In order to shave off a few pounds, we'll setup purge in the &lt;code&gt;tailwind.config.js&lt;/code&gt;file, which should end up looking like this.&lt;/p&gt;

&lt;p&gt;It will only keep classes present in your theme file (assuming you did name it &lt;code&gt;Theme+MyTheme.swift&lt;/code&gt; otherwise you need to adjust the content line).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
module.exports = {
  content: ['../../Sources/**/Theme+*.swift'],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [require('@tailwindcss/typography')],
}

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

&lt;/div&gt;



&lt;p&gt;Now we need to re-build the styles.css. Before you do it, check the number of lines in your current styles.css file so you can compare before/after the purge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
npx tailwindcss build theme.css -o styles.css -c tailwind.config.js

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

&lt;/div&gt;



&lt;p&gt;And with this, we are done!&lt;/p&gt;

&lt;h1&gt;
  
  
  Too much work?
&lt;/h1&gt;

&lt;p&gt;Don't worry, I got your back!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--C05Ks2AG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-02_at_23.12.40.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--C05Ks2AG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paraside.in/images/3/Screenshot_2020-08-02_at_23.12.40.png" alt="/images/3/Screenshot&amp;lt;em&amp;gt;2020-08-02&amp;lt;/em&amp;gt;at_23.12.40.png" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want a website that looks like this, just checkout my article &lt;a href="//../casperish"&gt;Your Publish website with my Casperish theme in less than 5 min&lt;/a&gt;, and then read &lt;a href="//../publish-on-github-pages"&gt;Deploy your Publish website for free on GitHub Pages&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>tailwindcss</category>
      <category>publish</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Deploy your Publish website for free on GitHub Pages</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Thu, 13 Aug 2020 13:10:00 +0000</pubDate>
      <link>https://dev.to/sowenjub/deploy-your-publish-website-for-free-on-github-pages-1nhn</link>
      <guid>https://dev.to/sowenjub/deploy-your-publish-website-for-free-on-github-pages-1nhn</guid>
      <description>&lt;h1&gt;
  
  
  🐙 Create a new GitHub repository
&lt;/h1&gt;

&lt;p&gt;First things first, let's create a repository to host your website content.&lt;/p&gt;

&lt;p&gt;We'll call it &lt;code&gt;mywebsite&lt;/code&gt; , which means it will be accessible at &lt;code&gt;https://yourusername.github.io/mywebsite&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of &lt;code&gt;mywebsite&lt;/code&gt;, if you use &lt;code&gt;[yourusername.github.io](http://yourusername.github.io)&lt;/code&gt; it will be accessible at &lt;code&gt;https://yourusername.github.io&lt;/code&gt;. See &lt;a href="https://pages.github.com"&gt;https://pages.github.com&lt;/a&gt; for more infos about that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7goQyr2i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_15.21.16.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7goQyr2i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_15.21.16.png" alt="/images/4/Screenshot&amp;lt;em&amp;gt;2020-08-11&amp;lt;/em&amp;gt;at_15.21.16.png" width="880" height="769"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  🎞 Setup the Publish pipeline
&lt;/h1&gt;

&lt;p&gt;Open your &lt;code&gt;main.swift&lt;/code&gt; file and add &lt;code&gt;deployedUsing: .gitHub("yourusername/mywebsite")&lt;/code&gt; to your publish pipeline, which should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
try MyWebsite().publish(
     withTheme: .casperish,
     deployedUsing: .gitHub("sowenjub/mywebsite"),
     additionalSteps: [
         .installPlugin(.readingTime()),
         .installPlugin(.pygments()),
     ],
     plugins: [.pygments()]
 )

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

&lt;/div&gt;



&lt;p&gt;Now run your site in Xcode (Product &amp;gt; Run command or ⌘+R), then open your terminal and run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
publish deploy

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

&lt;/div&gt;



&lt;p&gt;Head over to your repository, refresh and make sure your files are there.&lt;/p&gt;

&lt;h1&gt;
  
  
  🥁 Setup GitHub Pages
&lt;/h1&gt;

&lt;p&gt;Open your repository settings, scroll down to the GitHub Pages section and select the master branch. Don't forget to click the Save button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Z772rGI5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_15.28.14.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Z772rGI5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_15.28.14.png" alt="/images/4/Screenshot&amp;lt;em&amp;gt;2020-08-11&amp;lt;/em&amp;gt;at_15.28.14.png" width="880" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you're done, it should give your the address to your website.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YSzC5Fg2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_15.29.07.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YSzC5Fg2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_15.29.07.png" alt="/images/4/Screenshot&amp;lt;em&amp;gt;2020-08-11&amp;lt;/em&amp;gt;at_15.29.07.png" width="880" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And you're done! Unless you want to use a custom domain name, which can be set in the same settings panel.&lt;/p&gt;

&lt;h1&gt;
  
  
  🍒 Bonus: Deploying from Xcode
&lt;/h1&gt;

&lt;p&gt;Instead of deploy using the command line with &lt;code&gt;publish deploy&lt;/code&gt;, you can deploy straight from Xcode.&lt;/p&gt;

&lt;p&gt;First, create a New Scheme, we'll call it &lt;code&gt;Deploy MyWebsite&lt;/code&gt;. Click on it to reopen the same menu and select "Edit Scheme…".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nKCaENPr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_22.21.08.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nKCaENPr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_22.21.08.png" alt="/images/4/Screenshot&amp;lt;em&amp;gt;2020-08-11&amp;lt;/em&amp;gt;at_22.21.08.png" width="488" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select Run and in the Arguments Passed On Lauch, add &lt;code&gt;--deploy&lt;/code&gt; without space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nkhSLcvp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_22.24.05.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nkhSLcvp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paraside.in/images/4/Screenshot_2020-08-11_at_22.24.05.png" alt="/images/4/Screenshot&amp;lt;em&amp;gt;2020-08-11&amp;lt;/em&amp;gt;at_22.24.05.png" width="880" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now anytime your Run your website with the scheme (Product &amp;gt; Run command or ⌘+R), it will deploy your site to GitHub.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>publish</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Publish your website with my Casperish theme in less than 5 min</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Thu, 13 Aug 2020 12:59:00 +0000</pubDate>
      <link>https://dev.to/sowenjub/your-publish-website-with-my-casperish-theme-in-less-than-5-min-4m0b</link>
      <guid>https://dev.to/sowenjub/your-publish-website-with-my-casperish-theme-in-less-than-5-min-4m0b</guid>
      <description>&lt;p&gt;I ported Ghost's Casper theme (v2) so you don't have to: it's called CasperishTheme and is &lt;a href="https://github.com/sowenjub/CasperishTheme"&gt;available on GitHub&lt;/a&gt;. In this post explains how to install and customize it. It uses TailwindCSS and is based on &lt;a href="https://github.com/tailwindtoolbox/Ghostwind"&gt;Ghostwind&lt;/a&gt; with a few tweaks, upgrades and adaptations.&lt;/p&gt;

&lt;p&gt;I'll assume that you have Xcode and its Command Line Tools installed on your machine. I detailed in another blog post how to setup a Publish website with a TailwindCSS theme that you can customize to your taste, so I won't go into much details here.&lt;/p&gt;

&lt;h1&gt;
  
  
  What's included with this theme
&lt;/h1&gt;

&lt;p&gt;Porting the Casper theme was not only about mimicking its CSS but also about adopting its features while staying within what Publish can do (quite a lot with the help of some plugins).&lt;/p&gt;

&lt;p&gt;So this theme includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support for both Publish sections and pages (see "Organising your articles" at the end)&lt;/li&gt;
&lt;li&gt;Support for subfolder hosting (compatible with GitHub Pages or /blog path of an existing landing page)&lt;/li&gt;
&lt;li&gt;Icons for 5 social profiles: Dev.to, GitHub, LinkedIn, Stack Overflow, Twitter&lt;/li&gt;
&lt;li&gt;Reading time, shown at the bottom of articles cards&lt;/li&gt;
&lt;li&gt;Cover images for each article&lt;/li&gt;
&lt;li&gt;Beautiful typographic defaults for your posts thanks to TailwindCSS typography plugin&lt;/li&gt;
&lt;li&gt;A newsletter form (because why not?)&lt;/li&gt;
&lt;li&gt;Syntax Highligthing with a Monokai theme because if you're considering Publish it's likely you will blog about code one day or the other&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  ⬇️ Install Publish
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
git clone https://github.com/JohnSundell/Publish.git
cd Publish
make

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

&lt;/div&gt;



&lt;h1&gt;
  
  
  ✨ Create your website
&lt;/h1&gt;

&lt;p&gt;Let's get out of /Publish and create your website source next to it. We'll call it &lt;code&gt;MyWebsite&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
cd ..
mkdir MyWebsite
cd MyWebsite
publish new

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

&lt;/div&gt;



&lt;h1&gt;
  
  
  👻 Install the CasperishTheme
&lt;/h1&gt;

&lt;p&gt;Open Xcode&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
open Package.swift

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

&lt;/div&gt;



&lt;p&gt;Locate Package.swift manifest (it should be the first file) add the theme as a dependency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import PackageDescription

let package = Package(
    name: "MyWebsite",
    products: [
        .executable(
            name: "MyWebsite",
            targets: ["MyWebsite"]
        )
    ],
    dependencies: [
        .package(name: "Publish", url: "https://github.com/johnsundell/publish.git", from: "0.7.0"),
        .package(name: "CasperishTheme", url: "https://github.com/sowenjub/CasperishTheme.git", .branch("master")),
    ],
    targets: [
        .target(
            name: "MyWebsite",
            dependencies: ["Publish", "CasperishTheme"]
        )
    ]
)

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

&lt;/div&gt;



&lt;p&gt;Note: ideally, instead of using the master branch, we would use versioning as we do for publish itself. But we can't do that at the moment because of the syntax highlighting library, so in the meantime, we're using the master branch.&lt;/p&gt;

&lt;h1&gt;
  
  
  🧑‍🎨 Setup your website
&lt;/h1&gt;

&lt;p&gt;Open &lt;code&gt;MyWebsite/Sources/main.swift&lt;/code&gt; and customize it using the example below. The numbered comments pinpoint the changes required (more explanation below)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import Foundation
import Publish
import Plot
import CasperishTheme // 1

// This type acts as the configuration for your website.
struct MyWebsite: Website, CasperishWebsite { // 2
    enum SectionID: String, WebsiteSectionID {
        // Add the sections that you want your website to contain here:
        case posts
    }

    struct ItemMetadata: WebsiteItemMetadata, CasperishWebsiteItemMetadata { // 3
        // Add any site-specific metadata that you want to use here.
        var cover: String? // 3 bis
    }

    // Update these properties to configure your website:
    var url = URL(string: "https://your-website-url.com")!
    var name = "MyWebsite"
    var description = "A description of MyWebsite"
    var language: Language { .english }
    var imagePath: Path? { nil }

    // 4
    // Update these properties to configure your casperish-website:
    var rootPathString = "/"
    var headerColor = "#424242"
    var cover = ""
    var author = "Arnaud Joubay"
    var avatar = "http://i.pravatar.cc/300"
    var bio = "Swift &amp;amp; Rails Indie Maker"
    var icon = "🏝"
    var newsletterAction = ""
    var contacts: [(ContactPoint, String)] { [
        (.twitter, "sowenjub"),
        (.dev, "sowenjub"),
        (.linkedIn, "arnaudjoubay"),
        (.gitHub, "sowenjub"),
        (.stackoverflow, "229688"),
    ]}
}

// This will generate your website using the built-in Foundation theme:
try MyWebsite().publish(
     withTheme: .casperish,
     additionalSteps: [
         .installPlugin(.readingTime()),
         .installPlugin(.pygments()),
     ],
     plugins: [.pygments()]
 ) // 5

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Well, you need the CasperishTheme package obviously&lt;/li&gt;
&lt;li&gt;Your Website must adopt the &lt;code&gt;CasperishWebsite&lt;/code&gt; protocol, which enables theme specific configurations (see 4.)&lt;/li&gt;
&lt;li&gt;Your ItemMetadata must adopt the &lt;code&gt;CasperishWebsiteItemMetadata&lt;/code&gt; which will allow you to add cover photos to each of your posts (which are items in the Publish jargon). So we also need to add the line &lt;code&gt;var cover: String?&lt;/code&gt; inside.&lt;/li&gt;
&lt;li&gt;This is where you can really make this theme your own:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rootPathString&lt;/code&gt; this allows you to publish the website in a subfolder, such as on GitHub Pages without a custom domain name. Leave it to &lt;code&gt;"/"&lt;/code&gt; if your site will leave at the root domain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;headerColor&lt;/code&gt; hexadecimal code for the header's background-color. It will be hidden by the cover image if you use one&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cover&lt;/code&gt; optional path the to cover image. Leave blank (&lt;code&gt;""&lt;/code&gt;) if you want to use the &lt;code&gt;headerColor&lt;/code&gt; instead. If you have a cover.jpg image, the path should be &lt;code&gt;"/cover.jpg"&lt;/code&gt; if your image is at the root of your &lt;code&gt;/Resources&lt;/code&gt; folder (don't forget the / or it won't work on subpages).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;author&lt;/code&gt; and &lt;code&gt;bio&lt;/code&gt; are displayed just above the footer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;avatar&lt;/code&gt; is the path to your profile picture, displayed for each post. The path should be &lt;code&gt;"/my avatar.jpg"&lt;/code&gt; if your image is at the root of your &lt;code&gt;/Resources&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;icon&lt;/code&gt; can be an emoji or any short text suitable to be the icon in the navigation bar on mobile&lt;/li&gt;
&lt;li&gt;- &lt;code&gt;newsletterAction&lt;/code&gt; leave blank (&lt;code&gt;""&lt;/code&gt;) if you want to hide the newsletter, otherwise replace with the target url&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;contacts&lt;/code&gt; is an array of nicknames used to display links to your web profiles in the header (on desktop). For now, it only supports Twitter, Dev.to, LinkedIn, Github and Stack Overflow. 5. To generate your website you need to include the 2 plugins that ships with the theme&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  ⌨️ Edit your first post
&lt;/h1&gt;

&lt;p&gt;Open &lt;code&gt;Content/posts/first-post.md&lt;/code&gt; and write your first post!&lt;/p&gt;

&lt;h2&gt;
  
  
  Example of first post
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---
title: My first post
cover: /first_post.jpg
date: 2020-08-04 00:46
description: A description of my first post.
tags: first, article
---
# My first post's content title

My first post's text.

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

&lt;/div&gt;



&lt;p&gt;If you want to get up and running fast, you can cheat and replace &lt;code&gt;/first_post.jpg&lt;/code&gt; with &lt;a href="https://source.unsplash.com/collection/1118905/"&gt;&lt;code&gt;https://source.unsplash.com/collection/1118905/&lt;/code&gt;&lt;/a&gt; or even remove the line entirely and your post won't have a cover.&lt;/p&gt;

&lt;h1&gt;
  
  
  🚀 Run your website
&lt;/h1&gt;

&lt;p&gt;You're done! To see the result, just run the following command from &lt;code&gt;/MyWebsite&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
publish run

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

&lt;/div&gt;



&lt;p&gt;Now open your browser and enjoy the result.&lt;/p&gt;

&lt;p&gt;If you use this as your theme, ping me and I'll list your blog to the &lt;a href="https://github.com/sowenjub/CasperishTheme"&gt;CasperishTheme repository&lt;/a&gt;. ✌️&lt;/p&gt;

&lt;h1&gt;
  
  
  🍻 Bonus: things to know
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Posts cover images
&lt;/h3&gt;

&lt;p&gt;The theme allows posts to have cover images, you just have to set it in your front matter.&lt;/p&gt;

&lt;p&gt;So if you want to use &lt;code&gt;/Resources/first_post.jpg&lt;/code&gt; as you cover for a post, add &lt;code&gt;cover: /first_post.jpg&lt;/code&gt; like below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---
title: My first post
cover: /first_post.jpg
date: 2020-08-04 00:46
description: A description of my first post.
tags: life, anew
---
# My first post

My first post's text.

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

&lt;/div&gt;



&lt;p&gt;And if you don't want a cover, just remove the cover line!&lt;/p&gt;

&lt;h3&gt;
  
  
  Title
&lt;/h3&gt;

&lt;p&gt;Publish is smart: it will guess the title front your post content. But this means your title will appear twice: once above the cover image, and once below.&lt;/p&gt;

&lt;p&gt;Instead, you should set it in your front matter (as is done is the example above).&lt;/p&gt;

&lt;h3&gt;
  
  
  Organising your articles
&lt;/h3&gt;

&lt;p&gt;In order to organise your content, you can play two variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;your categories (displayed in the menu, declared in your &lt;code&gt;main.swift&lt;/code&gt; as SectionID)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;your tags (displayed above each post, declared in each post's front matter)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>swift</category>
      <category>tailwindcss</category>
      <category>publish</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Emoji picker controller with StimulusJS and Emoji Button</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Wed, 15 Jul 2020 14:31:43 +0000</pubDate>
      <link>https://dev.to/sowenjub/emoji-picker-controller-with-stimulusjs-and-emoji-button-5f6f</link>
      <guid>https://dev.to/sowenjub/emoji-picker-controller-with-stimulusjs-and-emoji-button-5f6f</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4tqxZAkL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/0rn1pcp9tlk73nzrnuki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4tqxZAkL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/0rn1pcp9tlk73nzrnuki.png" alt="Alt Text" width="778" height="794"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I needed an emoji picker for my latest Rails app and found &lt;a href="https://github.com/joeattardi/emoji-button"&gt;Emoji Button&lt;/a&gt; by &lt;a class="mentioned-user" href="https://dev.to/joeattardi"&gt;@joeattardi&lt;/a&gt; that he introduced &lt;a href="https://dev.to/joeattardi/emoji-button-a-vanilla-javascript-emoji-picker-1lf9"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since he already did the heavy-lifting required to provide a vanilla JavaScript emoji picker component, I just needed to add it to my app and create a Stimulus controller.&lt;br&gt;
And here's how to do it for yours.&lt;/p&gt;

&lt;p&gt;I'll assume you already have &lt;a href="https://stimulusjs.org"&gt;StimulusJS&lt;/a&gt; installed. From there, it's only a couple of steps.&lt;/p&gt;

&lt;p&gt;First, open your Terminal and add the emoji-button package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add @joeattardi/emoji-button
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, create a &lt;code&gt;app/javascript/controllers/emoji_picker_controller.js&lt;/code&gt; with this code inside&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"stimulus"&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="no"&gt;EmojiButton&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@joeattardi/emoji-button'&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;extends&lt;/span&gt; &lt;span class="no"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;static&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"button"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"input"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;picker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="no"&gt;EmojiButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;picker&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="s1"&gt;'emoji'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buttonTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt;
      &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inputTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;picker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;togglePicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;target&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;And now, if you have a &lt;code&gt;Post&lt;/code&gt; model with an &lt;code&gt;emoji&lt;/code&gt; attribute, you can do this in your form (the syntax below uses Slim instead of ERB):&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;form_for&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;post_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;controller: &lt;/span&gt;&lt;span class="s2"&gt;"emoji-picker"&lt;/span&gt; &lt;span class="p"&gt;}&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;button&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;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"emoji-picker#toggle"&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;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"emoji-picker.button"&lt;/span&gt;
    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;presence&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;content_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"fad fa-smile text-gray-400"&lt;/span&gt;&lt;span class="p"&gt;)&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;hidden_field&lt;/span&gt; &lt;span class="ss"&gt;:emoji&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"emoji-picker.input"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;content_tag(:i, nil, class: "fad fa-smile text-gray-400")&lt;/code&gt; bit uses &lt;a href="https://fontawesome.com"&gt;FontAwesome&lt;/a&gt; to show a default gray emoji when there is none.&lt;/p&gt;

&lt;p&gt;And that's all there is to it. Anytime you click the button, the picker will be toggled, and if you select an emoji, both the button and the hidden form input will be updated.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>stimulus</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Rails+TailwindCSS: adding "required" to form labels (and how to find the solution by yourself)</title>
      <dc:creator>Arnaud Joubay</dc:creator>
      <pubDate>Fri, 10 Jul 2020 13:44:26 +0000</pubDate>
      <link>https://dev.to/sowenjub/rails-tailwindcss-adding-required-to-form-labels-and-how-to-find-the-solution-by-yourself-1549</link>
      <guid>https://dev.to/sowenjub/rails-tailwindcss-adding-required-to-form-labels-and-how-to-find-the-solution-by-yourself-1549</guid>
      <description>&lt;p&gt;This post addresses a simple need: adding a "required" text next to any form field label that is… required.&lt;/p&gt;

&lt;p&gt;But I wanted a solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using only TailwindCSS/TailwindUI existing classes&lt;/li&gt;
&lt;li&gt;that can be reused easily&lt;/li&gt;
&lt;li&gt;internationalization friendly (because we're using a word - required - and not the "*" sign as you often see)&lt;/li&gt;
&lt;li&gt;that doesn't reinvent the wheel in some way&lt;/li&gt;
&lt;li&gt;that looks like this&lt;/li&gt;
&lt;/ul&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4isnogc6vypl829kl0k1.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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4isnogc6vypl829kl0k1.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  For the coder in a hurry, my solution
&lt;/h1&gt;

&lt;p&gt;The gist of it is &lt;code&gt;label_builder.translation&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&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;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"block text-xxs uppercase text-gray-500"&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;label_builder&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label_builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translation&lt;/span&gt;
  &lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="nc"&gt;.ml-2.normal-case.text-orange-400.text-xxs.font-semibold&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email_field&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;autocomplete: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"richard@piedpiper.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: I use the &lt;a href="https://github.com/slim-template/slim" rel="noopener noreferrer"&gt;Slim&lt;/a&gt; template language instead of ERB; it's easier to read so I trust that even if you've never heard of it you won't be lost.&lt;/p&gt;

&lt;h1&gt;
  
  
  For the curious mind, how I got there and why
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;FAIR WARNING: the rest is not your typical technical article. You already have the solution above, the rest is about how I found the solution. So it's more a story than a guide.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here I was setting up a simple profile form, adding the proper &lt;code&gt;required: true&lt;/code&gt; attributes and looking at it when I realized: &lt;strong&gt;how the hell would a user know which fields are required and which aren't?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Like anyone, anytime I see a form, my mind looks for the easy way out and tries to figure out how to hit that submit button as quickly as possible.&lt;br&gt;
This bland form triggered my fill or flight response and I knew I had to do something about it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Starting point
&lt;/h2&gt;

&lt;p&gt;With a bit of TailwindCSS, here is what it was looking like.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1eyy0fn26yk5ozi1knph.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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1eyy0fn26yk5ozi1knph.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and the code&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&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;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"block text-xxs uppercase text-gray-500"&lt;/span&gt;
&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email_field&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;autocomplete: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"richard@piedpiper.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's as basic as it gets: a label and an email_field with some classes.&lt;br&gt;
I will skip the TailwindCSS explanations, just know that "form-input" comes from a &lt;a href="https://tailwindcss-custom-forms.netlify.app" rel="noopener noreferrer"&gt;plugin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Goal check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ using only TailwindCSS/TailwindUI existing classes&lt;/li&gt;
&lt;li&gt;❌ that can be reused easily&lt;/li&gt;
&lt;li&gt;❌ internationalization friendly&lt;/li&gt;
&lt;li&gt;❌ that doesn't reinvent the wheel in some way&lt;/li&gt;
&lt;li&gt;❌ that looks like the cover image&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Approach n°1: Add a "*" after the email
&lt;/h2&gt;

&lt;p&gt;Often, forms signal that a field is required by appending an asterisk to the label title, which can be done with a bit of CSS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;  &lt;span class="nc"&gt;.required&lt;/span&gt;&lt;span class="nd"&gt;:after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;content&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I went to the &lt;a href="https://tailwindcss.com/docs/installation" rel="noopener noreferrer"&gt;TailwindCSS docs&lt;/a&gt; and couldn't find anything ready-made to handle this.&lt;br&gt;
Sure, I could add it to my stylesheet files, but I already had another idea in mind.&lt;br&gt;
I played a little bit with &lt;a href="https://basecamp.com" rel="noopener noreferrer"&gt;Basecamp&lt;/a&gt; recently as I watched the &lt;a href="https://www.youtube.com/playlist?list=PL9wALaIpe0Py6E_oHCgTrD6FvFETwJLlx" rel="noopener noreferrer"&gt;On Writing Software (well?)&lt;/a&gt; videos by &lt;a href="http://twitter.com/dhh" rel="noopener noreferrer"&gt;DHH&lt;/a&gt;. And it reminded me that using the word "required" instead of a simple "*" sign is great to improve clarity.&lt;/p&gt;
&lt;h2&gt;
  
  
  Approach n°2: Appeal to humanity
&lt;/h2&gt;

&lt;p&gt;This confusing title is a bad pun inspired the method we'll be using in this approach: &lt;code&gt;Model.human_attribute_name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;label&lt;/code&gt; can take a block to render, it's easy to come up with a first solution.&lt;/p&gt;

&lt;p&gt;So I went from&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&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;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"block text-xxs uppercase text-gray-500"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&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;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"block text-xxs uppercase text-gray-500"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt; Email
  &lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="nc"&gt;.ml-2.normal-case.text-orange-400.text-xxs.font-semibold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OK. It looks like the final result, but &lt;strong&gt;we're not quite there yet, because both "Email" and "required" are hardcoded&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Goal check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ using only TailwindCSS/TailwindUI existing classes&lt;/li&gt;
&lt;li&gt;❌ that can be reused easily&lt;/li&gt;
&lt;li&gt;❌ internationalization friendly&lt;/li&gt;
&lt;li&gt;❌ that doesn't reinvent the wheel in some way&lt;/li&gt;
&lt;li&gt;✅ that looks like the cover image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "required" text doesn't need much talking about. We just have to replace it with &lt;code&gt;= t("required")&lt;/code&gt; and add a translation somewhere, probably in &lt;code&gt;config/locals/en.yml&lt;/code&gt; since it will be pretty generic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what about the email? I needed a solution that would be a bit more generic.&lt;br&gt;
The &lt;a href="https://guides.rubyonrails.org/i18n.html#translations-for-active-record-models" rel="noopener noreferrer"&gt;standard way to look up translations&lt;/a&gt; for any attribute is &lt;code&gt;Model.human_attribute_name(:attribute)&lt;/code&gt;, so I did just that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&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;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"block text-xxs uppercase text-gray-500"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c"&gt;/ 👍 Not horrible&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="nc"&gt;.ml-2.normal-case.text-orange-400.text-xxs.font-semibold&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email_field&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;autocomplete: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"richard@piedpiper.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, I am relying on common techniques. A little bit unsatisfying but it gets the job done.&lt;/p&gt;

&lt;p&gt;What did I find unsatisfying, you may wonder?&lt;/p&gt;

&lt;p&gt;Generally speaking, rails take care of many intricacies you might not think of. When you're replicating part of a method (in our case the part that takes a symbol - :email - and turns it into text - Email -), it's highly likely you're forgetting edge cases or oversimplifying.&lt;br&gt;
It's like using a steering wheel versus ropes tied to the wheels. Sure, it works, you have a direct grip on things, but there's a reason we built an intermediary thingy.&lt;/p&gt;

&lt;p&gt;In particular, by doing our own thing instead of relying on rails' wisdom, we're missing out on lazy lookup cleverness.&lt;br&gt;
And in our case, by using &lt;code&gt;Model.human_attribute_name&lt;/code&gt; directly, we are reinventing the wheel.&lt;/p&gt;

&lt;p&gt;Goal check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ using only TailwindCSS/TailwindUI existing classes&lt;/li&gt;
&lt;li&gt;✅ that can be reused easily&lt;/li&gt;
&lt;li&gt;✅ internationalization friendly&lt;/li&gt;
&lt;li&gt;❌ that doesn't reinvent the wheel in some way&lt;/li&gt;
&lt;li&gt;✅ that looks like the cover image&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Final approach
&lt;/h2&gt;

&lt;p&gt;Where to go from there? I knew that the &lt;code&gt;label&lt;/code&gt; helper was handling the translation at some level, and I wanted to know if you could tap into it.&lt;/p&gt;

&lt;p&gt;There was two way to deal with this. The easy way, and the way I did it because… I got carried away.&lt;/p&gt;
&lt;h3&gt;
  
  
  Let's start with the laborious (but still interesting) way
&lt;/h3&gt;

&lt;p&gt;The easiest way to know how the &lt;code&gt;label&lt;/code&gt; helper handles the translation is to look at the source code &lt;br&gt;
Using &lt;a href="https://kapeli.com/dash" rel="noopener noreferrer"&gt;Dash&lt;/a&gt;, I opened the rails source code and found &lt;a href="https://github.com/rails/rails/blob/b738f1930f3c82f51741ef7241c1fee691d7deb2/actionview/lib/action_view/helpers/form_helper.rb#L2252" rel="noopener noreferrer"&gt;this&lt;/a&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;def&lt;/span&gt; &lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&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="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&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;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@object_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;method&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="n"&gt;objectify_options&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&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;I clicked the label call and looked at the definitions found by Github's code navigation (I only discovered recently that Github lets you do that, so I'm mentioning it here in case you didn't know about).&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fg5dzkaz8ga9tsg0misky.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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fg5dzkaz8ga9tsg0misky.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The method's signature of the &lt;a href="https://github.com/rails/rails/blob/a0e0f0263902896a7aacf703bcab35bee16bdaf8/actionview/lib/action_view/helpers/form_helper.rb#L1118" rel="noopener noreferrer"&gt;first row&lt;/a&gt; seems to match the one used before, so I followed it and found this, which doesn't do much:&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;def&lt;/span&gt; &lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content_or_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Tags&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Label&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;object_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content_or_options&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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&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;From there, I followed &lt;code&gt;Tags::Label&lt;/code&gt; and finally arrived on the &lt;a href="https://github.com/rails/rails/blob/a0e0f0263902896a7aacf703bcab35bee16bdaf8/actionview/lib/action_view/helpers/tags/label.rb#L6" rel="noopener noreferrer"&gt;LabelBuilder&lt;/a&gt;&lt;br&gt;
Here I started to skim through the code, but I didn't have to go far because, at the very top, I saw a promising &lt;code&gt;def translation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I went back to the code, added a parameter to the block call that I felt should be named label_builder, and called its translation method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&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;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"block text-xxs uppercase text-gray-500"&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;label_builder&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c"&gt;/ ✨ True magic&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label_builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translation&lt;/span&gt;
  &lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="nc"&gt;.ml-2.normal-case.text-orange-400.text-xxs.font-semibold&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email_field&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;autocomplete: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"richard@piedpiper.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This translation is closer to the template engine than the previous solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  The easy way
&lt;/h3&gt;

&lt;p&gt;Before we go further, here is the easy way to find that same method in less time.&lt;/p&gt;

&lt;p&gt;You just have to a) use the console&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&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;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"block text-xxs uppercase text-gray-500"&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;label_builder&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;console&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) load the page in your browser, wait for the console to appear, and type &lt;code&gt;label_builder.methods&lt;/code&gt; to return the list of the names of methods of our label_builder: &lt;code&gt;:translation&lt;/code&gt; is the very first one.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu22smpon0xpra2yqk1ml.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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu22smpon0xpra2yqk1ml.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why didn't I think of that? Force of habits mostly.&lt;br&gt;
I've found a lot of solutions recently by reading code so it was my first instinct. Also, I didn't think it would be that easy, ie that the &lt;code&gt;LabelBuilder&lt;/code&gt; would just expose the translation.&lt;br&gt;
But I must admit that the prospect of reading code and learning a thing or two because of it was also appealing. I find that it's always worth my time.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why is it better and what did we learn?
&lt;/h3&gt;

&lt;p&gt;Remember when I told you that we were missing out on lazy lookup cleverness?&lt;br&gt;
You can reveal it using &lt;a href="https://github.com/fphilipe/i18n-debug" rel="noopener noreferrer"&gt;i18n-debug&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The previous solution using &lt;code&gt;human_attribute_name&lt;/code&gt; looks for this key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;en.activerecord.attributes.contact.email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our new solution first looks for one specific to labels and only falls back on the model one if it's nil.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;en.helpers.label.contact.email
en.activerecord.attributes.contact.email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is way more satisfying 😌 and the end of the original article.&lt;/p&gt;

&lt;p&gt;I wanted to not only share my solution but also the tools I use as well as a way to go beyond duck-taping (which to me is any approach up to - and including - the &lt;code&gt;human_attribute_name&lt;/code&gt; approach).&lt;/p&gt;

&lt;p&gt;At this point you know how to add a "required" text to form labels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ using only TailwindCSS/TailwindUI existing classes&lt;/li&gt;
&lt;li&gt;✅ that can be reused easily&lt;/li&gt;
&lt;li&gt;✅ internationalization friendly&lt;/li&gt;
&lt;li&gt;✅ that doesn't reinvent the wheel in some way&lt;/li&gt;
&lt;li&gt;✅ that looks like the cover image&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  For the Stakhanovites, going further with a Form Builder
&lt;/h1&gt;

&lt;p&gt;I didn't plan to go that far initially, but I couldn't resist once I thought about it 😬&lt;/p&gt;

&lt;p&gt;If you're going to do that often, you'll probably want to start &lt;a href="https://guides.rubyonrails.org/form_helpers.html#customizing-form-builders" rel="noopener noreferrer"&gt;customizing form builders&lt;/a&gt; in order to make all of this even more reusable, write less code and get even closer to vanilla rails.&lt;/p&gt;

&lt;p&gt;Indeed, with a form builder we can simply write something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&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;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"block text-xxs uppercase text-gray-500"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required_class: &lt;/span&gt;&lt;span class="s2"&gt;"ml-2 normal-case text-orange-400 text-xxs font-semibold"&lt;/span&gt;
&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email_field&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;autocomplete: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &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;class: &lt;/span&gt;&lt;span class="s2"&gt;"form-input mt-1 block w-full text-field focus:shadow-outline focus:border-green-300"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"richard@piedpiper.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you compare it to our starting point, you'll see that there is only one difference: an extra &lt;code&gt;required_class&lt;/code&gt;. &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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fj03a8ptbymo67f37whm2.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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fj03a8ptbymo67f37whm2.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, this is 😎.&lt;/p&gt;

&lt;p&gt;What would that form builder look like? Here's my take on this. It's the first form builder I write, so if you have more experienced and notice something weird please do tell.&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/form_builders/requiring_form_builder.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RequiringFormBuilder&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionView&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Helpers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FormBuilder&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&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="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&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;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text_is_options&lt;/span&gt; &lt;span class="o"&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;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="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text_is_options&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="ss"&gt;:required&lt;/span&gt;&lt;span class="p"&gt;]&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="ss"&gt;:required&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;required&lt;/span&gt;
      &lt;span class="n"&gt;required_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text_is_options&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;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:required_class&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:required_class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&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="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&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;label_builder&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="vi"&gt;@template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="n"&gt;label_builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translation&lt;/span&gt;
        &lt;span class="vi"&gt;@template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="vi"&gt;@template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: :helpers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="n"&gt;required_class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="vi"&gt;@template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label_builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;block_given?&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&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="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&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="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two other things required for this to work.&lt;/p&gt;

&lt;p&gt;First, the form_with declaration must declare the builder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt; &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;builder: &lt;/span&gt;&lt;span class="no"&gt;RequiringFormBuilder&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, you might have noticed that I use &lt;code&gt;I18n.t("required", scope: :helpers)&lt;/code&gt; and not &lt;code&gt;I18n.t("required")&lt;/code&gt; as we did before because it seems more appropriate to put that under the &lt;code&gt;helpers&lt;/code&gt; namespace (in my case in a file called &lt;code&gt;config/locals/helpers/en.yml&lt;/code&gt;). So you need to move that in the proper translation file, or at the minimum to change&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;helpers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I won't go into the details, but I'll point to two parts in the rails code that helped me write this code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/rails/rails/blob/a0e0f0263902896a7aacf703bcab35bee16bdaf8/actionview/lib/action_view/helpers/tags/label.rb#L30" rel="noopener noreferrer"&gt;https://github.com/rails/rails/blob/a0e0f0263902896a7aacf703bcab35bee16bdaf8/actionview/lib/action_view/helpers/tags/label.rb#L30&lt;/a&gt; because text can, in fact, be the options&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/rails/rails/blob/477fae3eb3d3b3bfdbe28586fecb8578c0be4721/actionview/lib/action_view/helpers/form_helper.rb#L1924" rel="noopener noreferrer"&gt;https://github.com/rails/rails/blob/477fae3eb3d3b3bfdbe28586fecb8578c0be4721/actionview/lib/action_view/helpers/form_helper.rb#L1924&lt;/a&gt; because we don't want to break block calling (&lt;code&gt;do |label_builder|&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this form builder, we can now simply add a &lt;code&gt;required_class&lt;/code&gt; to any required label field.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article, you can follow me on Twitter &lt;a href="https://twitter.com/sowenjub" rel="noopener noreferrer"&gt;@sowenjub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>tailwindcss</category>
      <category>form</category>
      <category>i18n</category>
    </item>
  </channel>
</rss>
