<?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: Daniel Schreiber</title>
    <description>The latest articles on DEV Community by Daniel Schreiber (@danielsc).</description>
    <link>https://dev.to/danielsc</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%2F197892%2F2999b23e-6337-4d71-8164-9172c724cb57.jpeg</url>
      <title>DEV Community: Daniel Schreiber</title>
      <link>https://dev.to/danielsc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danielsc"/>
    <language>en</language>
    <item>
      <title>Affiliate opportunity for dev creators (B2B): Help teams ship i18n, earn 25% lifetime</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Fri, 15 Aug 2025 09:28:52 +0000</pubDate>
      <link>https://dev.to/danielsc/affiliate-opportunity-for-dev-creators-b2b-help-teams-ship-i18n-earn-25-lifetime-4h44</link>
      <guid>https://dev.to/danielsc/affiliate-opportunity-for-dev-creators-b2b-help-teams-ship-i18n-earn-25-lifetime-4h44</guid>
      <description>&lt;p&gt;Hey DEV folks 👋&lt;/p&gt;

&lt;p&gt;We run &lt;strong&gt;&lt;a href="https://doloc.io" rel="noopener noreferrer"&gt;doloc&lt;/a&gt;&lt;/strong&gt;, a developer-first localization tool that plugs into your existing build. If your audience is devs/solo-founders/app builders, our &lt;strong&gt;&lt;a href="https://doloc.io/affiliate/" rel="noopener noreferrer"&gt;affiliate program&lt;/a&gt;&lt;/strong&gt; might be a great fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why devs like doloc
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stays in the toolchain&lt;/strong&gt;: run translations as a &lt;strong&gt;single curl/API step after extraction&lt;/strong&gt; (no heavy platform).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent style&lt;/strong&gt;: uses your existing translations as an &lt;em&gt;implicit glossary&lt;/em&gt; so new strings match prior wording.&lt;/li&gt;
&lt;li&gt;Works nicely with &lt;strong&gt;Angular&lt;/strong&gt;, &lt;strong&gt;FormatJS/React-Intl&lt;/strong&gt;, &lt;strong&gt;Android&lt;/strong&gt;, &lt;strong&gt;Flutter&lt;/strong&gt;! Also a &lt;strong&gt;JetBrains IDEs&lt;/strong&gt; plugin is available.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want examples for your content:&lt;br&gt;
• &lt;strong&gt;&lt;a href="https://doloc.io/getting-started/frameworks/angular/" rel="noopener noreferrer"&gt;Angular guide&lt;/a&gt;&lt;/strong&gt; (how to wire extraction + doloc)&lt;br&gt;
• &lt;strong&gt;&lt;a href="https://doloc.io/getting-started/frameworks/react-intl-format-js/" rel="noopener noreferrer"&gt;FormatJS/React-Intl guide&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
• &lt;strong&gt;&lt;a href="https://doloc.io/getting-started/frameworks/flutter/" rel="noopener noreferrer"&gt;Flutter guide&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
These make solid tutorial posts/videos.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Affiliate terms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;25% lifetime recurring&lt;/strong&gt; commission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90-day cookie&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regular PayPal payouts&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;We’ll share logos, sample copy, and are happy to co-write a tutorial. Apply here: &lt;strong&gt;&lt;a href="https://doloc.io/affiliate/" rel="noopener noreferrer"&gt;doloc.io/affiliate&lt;/a&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Content ideas you can riff on
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;“Shift Left: i18n in sync with development”&lt;/li&gt;
&lt;li&gt;“CI-friendly i18n: translations as part of your pipeline”&lt;/li&gt;
&lt;li&gt;“From one locale to five in an afternoon”&lt;/li&gt;
&lt;li&gt;“Keeping translation style consistent without a manual glossary”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this fits your audience, we’d love to partner. Questions welcome — happy to share demo projects and code snippets.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This is an affiliate recruitment post for our own product.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>localization</category>
      <category>i18n</category>
      <category>affiliate</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Sun, 02 Mar 2025 10:39:25 +0000</pubDate>
      <link>https://dev.to/danielsc/-m60</link>
      <guid>https://dev.to/danielsc/-m60</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/danielsc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F197892%2F2999b23e-6337-4d71-8164-9172c724cb57.jpeg" alt="danielsc"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/danielsc/deep-dive-xml-trueformat-preserve-xml-formatting-with-ease-46f5" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Deep Dive: xml-trueformat – Preserve XML Formatting with Ease&lt;/h2&gt;
      &lt;h3&gt;Daniel Schreiber ・ Mar 1&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#xml&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#typescript&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>xml</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Deep Dive: xml-trueformat – Preserve XML Formatting with Ease</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Sat, 01 Mar 2025 18:39:58 +0000</pubDate>
      <link>https://dev.to/danielsc/deep-dive-xml-trueformat-preserve-xml-formatting-with-ease-46f5</link>
      <guid>https://dev.to/danielsc/deep-dive-xml-trueformat-preserve-xml-formatting-with-ease-46f5</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/daniel-sc/xml-trueformat" rel="noopener noreferrer"&gt;xml-trueformat&lt;/a&gt;&lt;/strong&gt; is a TypeScript library for parsing and manipulating XML documents &lt;strong&gt;while retaining their exact original formatting&lt;/strong&gt;. It stores whitespace, line breaks, comment placement, and attribute order—ensuring a no-op parse/serialize if you don’t change anything, and only minimal diffs when you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Formatting Preservation Matters
&lt;/h2&gt;

&lt;p&gt;Many XML tools strip “insignificant” whitespace or reformat tags. For files where human-friendly layout and indentation matter—like configuration files, manifests, or annotated XML templates—this can be disruptive. &lt;strong&gt;xml-trueformat&lt;/strong&gt; preserves every nuance of an XML file, so you can add elements or attributes programmatically &lt;strong&gt;without&lt;/strong&gt; rewriting everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to (not) Use
&lt;/h2&gt;

&lt;p&gt;Clearly, xml-trueformat satisfies a specific set of requirements - the following outlines, in which situations it fits well and where you should rather go for another parser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use xml-trueformat&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You require &lt;strong&gt;exact round-trip&lt;/strong&gt; output, preserving whitespace, comments, and attribute ordering.&lt;/li&gt;
&lt;li&gt;You handle &lt;strong&gt;configuration or manifest files&lt;/strong&gt; under version control where small diffs are critical.&lt;/li&gt;
&lt;li&gt;You need to &lt;strong&gt;insert or remove&lt;/strong&gt; nodes while leaving everything else unaltered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not ideal if&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You only need to &lt;strong&gt;parse XML data&lt;/strong&gt; (no need for original layout) or convert to JSON.&lt;/li&gt;
&lt;li&gt;You want &lt;strong&gt;fast, large-scale data extraction&lt;/strong&gt; from huge XML files (performance overhead is higher here).&lt;/li&gt;
&lt;li&gt;You prefer a simpler JSON-like object structure, e.g., from &lt;code&gt;xml2js&lt;/code&gt;, and &lt;strong&gt;don’t care&lt;/strong&gt; about formatting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Comparison with Other XML Parsers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/xml2js" rel="noopener noreferrer"&gt;xml2js&lt;/a&gt;&lt;/strong&gt;: Converts XML to a JavaScript object, loses formatting and comments. Simpler for data but no layout fidelity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/fast-xml-parser" rel="noopener noreferrer"&gt;fast-xml-parser&lt;/a&gt;&lt;/strong&gt;: High performance, optional “preserveOrder” for node sequence, but still not guaranteed exact whitespace or comments in original positions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOM-based parsers (&lt;a href="https://www.npmjs.com/package/xmldom" rel="noopener noreferrer"&gt;xmldom&lt;/a&gt;, etc.)&lt;/strong&gt;: May preserve some whitespace, but typically not attribute quotes/order or small spacing nuances. Re-serialization often changes formatting.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/sax" rel="noopener noreferrer"&gt;sax-js&lt;/a&gt;: An event-based streaming parser that’s great for processing large XML on-the-fly. It doesn’t build a modifiable DOM nor preserve formatting. Ideal for fast reads, not for round-trip exactness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;xml-trueformat&lt;/strong&gt; is purpose-built for retaining &lt;strong&gt;everything&lt;/strong&gt;. In exchange, it’s less streamlined for pure data transformations and may be slower for large files compared to specialized parsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Under the hood, xml-trueformat uses its own AST (abstract syntax tree) rather than a standard DOM. &lt;strong&gt;Every&lt;/strong&gt; piece of whitespace (indentation, newlines, spacing around attributes) is modeled as text nodes, plus specialized classes for comments, CDATA, etc. When you add or remove something, it automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matches Indentation&lt;/strong&gt; of sibling nodes if possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserves Quoting Style&lt;/strong&gt; from existing attributes (e.g., &lt;code&gt;' '&lt;/code&gt; or &lt;code&gt;" "&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeps Comments and Processing Instructions&lt;/strong&gt; precisely where they were.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distinguishes between self-closing and non-self-closing elements&lt;/strong&gt; (e.g. &lt;code&gt;&amp;lt;tag/&amp;gt;&lt;/code&gt; vs. &lt;code&gt;&amp;lt;tag&amp;gt;&amp;lt;/tag&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Smart Formatting on Modifications
&lt;/h2&gt;

&lt;p&gt;The real magic of xml-trueformat is that when you &lt;strong&gt;insert new elements or attributes&lt;/strong&gt;, it doesn't just plop them in arbitrarily – it &lt;strong&gt;matches the existing formatting style&lt;/strong&gt;. Two helper methods illustrate this well: adding a new element and adding a new attribute (we'll only show the first here).&lt;/p&gt;

&lt;h3&gt;
  
  
  Inserting Elements without Breaking Indentation
&lt;/h3&gt;

&lt;p&gt;Let’s say you have an XML list of entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;users&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;user&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;user&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Bob"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/users&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to add a new &lt;code&gt;&amp;lt;user&amp;gt;&lt;/code&gt; element programmatically, you’d want it indented with the same 4 spaces as the others, and on its own new line. With xml-trueformat, you can do something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;XmlElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;XmlAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Charlie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt; 
&lt;span class="nx"&gt;userElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there are sibling elements, xml-trueformat checks their whitespace usage (like line breaks and indentation) and applies the same to &lt;code&gt;&amp;lt;user&amp;gt;&lt;/code&gt;.  This results in a well formatted result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;users&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;user&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;user&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Bob"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;user&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Charlie"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/users&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course you could as well &lt;strong&gt;manually control formatting&lt;/strong&gt;, by using &lt;code&gt;XmlElement.addChild&lt;/code&gt; which does not perform any "smart" formatting and gives you &lt;strong&gt;full control&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;XmlParser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xml-trueformat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xmlData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example.xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;XmlParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xmlData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Modify attributes &lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRootElement&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setAttributeValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Add new element &lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newElem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;XmlElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;feature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;XmlAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRootElement&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;addElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newElem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Serialize &lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output.xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output remains faithful to the original indentation and spacing, with only the changes you requested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Perks and Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Comments and CDATA preserved:&lt;/strong&gt; Comments, processing instructions, and CDATA sections are not lost or reformatted. They are part of the object model (e.g., there are &lt;code&gt;XmlComment&lt;/code&gt; and &lt;code&gt;XmlCData&lt;/code&gt; node types) and will round-trip through parse and serialize intact, at their original places. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No heavy dependencies:&lt;/strong&gt; xml-trueformat is implemented in plain TypeScript with &lt;strong&gt;no external libraries required&lt;/strong&gt;. This makes it lightweight and ensures compatibility in Node.js and in browsers. You can use it in a backend script or on a frontend page – anywhere you need to manipulate XML reliably.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Minimal footprint of change:&lt;/strong&gt; When you do make modifications, xml-trueformat keeps the scope of changes minimal. If you diff the before vs after XML files, you’ll typically see only the lines related to your actual change. This makes code reviews and merges smoother.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;xml-trueformat&lt;/strong&gt; is an excellent choice for &lt;strong&gt;high-fidelity&lt;/strong&gt; XML editing, especially where minor layout changes would be problematic. It’s not the fastest or simplest for data extraction, but if you want minimal diffs and lossless round trips, it’s hard to beat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Questions or Feedback?
&lt;/h3&gt;

&lt;p&gt;Let me know what features or improvements you’d like. Feel free to open an issue or PR on &lt;a href="https://github.com/daniel-sc/xml-trueformat" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — feedback is always welcome!&lt;/p&gt;

</description>
      <category>xml</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Define, Generate, and Implement: An API-First Approach with OpenAPI Generator and FlightPHP</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Mon, 24 Feb 2025 10:34:26 +0000</pubDate>
      <link>https://dev.to/danielsc/define-generate-and-implement-an-api-first-approach-with-openapi-generator-and-flightphp-1fb3</link>
      <guid>https://dev.to/danielsc/define-generate-and-implement-an-api-first-approach-with-openapi-generator-and-flightphp-1fb3</guid>
      <description>&lt;p&gt;Adopting an API-first strategy ensures that your server and client remain in sync, dramatically reducing integration issues. By defining your API contract upfront, you can automatically generate both server stubs and client SDKs. This not only minimizes manual work but also creates a "typesafe" bridge between your front-end and back-end -- something even PHP developers can appreciate ;-)&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining Your API
&lt;/h2&gt;

&lt;p&gt;Begin by describing your API in an OpenAPI specification (e.g., my_api.yaml). In your spec, define endpoints, request/response schemas, and authentication details. For instance, a simple user endpoint might be defined as follows:&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;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.0.0&lt;/span&gt;
&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Example API&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;/users/{id}&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Retrieve a user by ID&lt;/span&gt;
      &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;id&lt;/span&gt;
          &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;200'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A user object&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/User'&lt;/span&gt;
    &lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update a user by ID&lt;/span&gt;
      &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;id&lt;/span&gt;
          &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
      &lt;span class="na"&gt;requestBody&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="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/User'&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;204'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;User updated&lt;/span&gt;
&lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schemas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;UserState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;active&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;disabled&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pending&lt;/span&gt;
    &lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;id&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;state&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/UserState'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this might seem bloated at first glance, bear with me — this approach makes subsequent implementations much easier and more robust!&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the Server Stub and Client SDK
&lt;/h2&gt;

&lt;p&gt;With your API defined, use the &lt;a href="https://openapi-generator.tech/" rel="noopener noreferrer"&gt;OpenAPI Generator&lt;/a&gt; to generate your code automatically. The PHP Flight generator — &lt;a href="https://openapi-generator.tech/docs/generators/php-flight/" rel="noopener noreferrer"&gt;documented here&lt;/a&gt; — was provided by the author and, although its status is still marked as "experimental", it has been my production workhorse for over a year.&lt;/p&gt;

&lt;p&gt;Here’s an example shell script that automates the generation of a FlightPHP server stub:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;OPEN_API_GEN_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"7.9.0"&lt;/span&gt;
&lt;span class="nv"&gt;GENERATOR_JAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"openapi-generator-cli-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OPEN_API_GEN_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.jar"&lt;/span&gt;

&lt;span class="c"&gt;# Download the generator if needed:&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GENERATOR_JAR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GENERATOR_JAR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OPEN_API_GEN_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/openapi-generator-cli-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OPEN_API_GEN_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.jar"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Clean previous outputs&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; generated-server

&lt;span class="c"&gt;# Generate a FlightPHP server stub using the php-flight generator&lt;/span&gt;
java &lt;span class="nt"&gt;-jar&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GENERATOR_JAR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; generate &lt;span class="nt"&gt;-i&lt;/span&gt; my_api.yaml &lt;span class="nt"&gt;-g&lt;/span&gt; php-flight &lt;span class="nt"&gt;-o&lt;/span&gt; generated-server &lt;span class="nt"&gt;--additional-properties&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;invokerPackage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;GeneratedApi,composerPackageName&lt;span class="o"&gt;=&lt;/span&gt;myapi/server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, you can similarly generate your API (frontend) clients. This automated process ensures that any changes to your API spec are consistently propagated across all layers, keeping your integration robust and typesafe. You can choose whether to track these generated files in version control or simply re-generate them in your CI build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check Out the Generated Source
&lt;/h2&gt;

&lt;p&gt;The generated files form a complete composer package, that you could package and use as dependency. Here we'll keep it in a subfolder and include it as a &lt;a href="https://getcomposer.org/doc/05-repositories.md#path" rel="noopener noreferrer"&gt;local composer repository&lt;/a&gt;. Assume you generated the following into a folder &lt;code&gt;generated-server&lt;/code&gt; in you project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── Api
│   └── AbstractDefaultApi.php
├── Model
│   ├── User.php
│   └── UserState.php
├── README.md
├── RegisterRoutes.php
├── Test
│   └── RegisterRoutesTest.php
├── composer.json
└── phpunit.xml.dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can require this in your &lt;code&gt;composer.json&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  // other properties ...
  "repositories": [
    {
      "type": "path",
      "url": "generated-server/"
    }
  ],
  "require": {
    "myapi/server": "*",
    // other dependencies ...
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The User model, for example, looks like this (without comments for better readability):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;GeneratedApi\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;\JsonSerializable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;UserState&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UserState&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fromArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;self&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'state'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;UserState&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;tryFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'state'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;jsonSerialize&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="s1"&gt;'state'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementing Your API Endpoints
&lt;/h2&gt;

&lt;p&gt;Once your server stub is generated, extend the generated abstract API classes to implement your business logic. The generated API classes contain method stubs with correct parameter and return types for every path from your API specification. An example implementation for the User API might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApp\Api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;GeneratedApi\Api\AbstractDefaultApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;GeneratedApi\Model\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;GeneratedApi\Model\UserState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserApi&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractDefaultApi&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[\Override]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;usersIdGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Simulate fetching a user from a data source&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Alice Smith'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UserState&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;Flight&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;halt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"User not found"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;\Override&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;usersIdPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="c1"&gt;// store user in DB - might use $user-&amp;gt;jsonSerialize() for serialization&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;Note: You do not need to overwrite all methods! If you like to handle some routes manually, just ignore these method stubs (and manually register these routes with FlightPHP).&lt;/p&gt;

&lt;p&gt;After implementing your API logic, register your endpoints with FlightPHP to integrate them into your application routing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="c1"&gt;// In your Flight bootstrap file&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;MyApp\Api\UserApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;GeneratedApi\RegisterRoutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;RegisterRoutes&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;registerRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserApi&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; 
&lt;span class="c1"&gt;// this is the place you'd manually invoke `Flight::route()`&lt;/span&gt;

&lt;span class="nc"&gt;Flight&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ties your custom implementation to FlightPHP’s routing system, ensuring that your API behaves as defined. The &lt;code&gt;RegisterRoutes::registerRoutes()&lt;/code&gt; method is generated by the generator and automatically checks which methods were implemented in &lt;code&gt;UserApi&lt;/code&gt; and registers them with &lt;code&gt;Flight:route()&lt;/code&gt; for the correct paths. Also it  de-/serializes request/response objects for you.&lt;/p&gt;

&lt;p&gt;Note all the things you &lt;em&gt;don't&lt;/em&gt; need to do with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No manual de-/serialization needed (you get your parameters in correct type - e.g. &lt;code&gt;userId&lt;/code&gt; as &lt;code&gt;int&lt;/code&gt;!)&lt;/li&gt;
&lt;li&gt;No manual checking if response/request is as expected (conforms to schema)&lt;/li&gt;
&lt;li&gt;No manual set up of paths/routes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;By following an API-first approach with OpenAPI Generator and FlightPHP, you ensure a consistent, typesafe contract between your front-end and back-end. While the initial setup might take a little effort, it quickly pays off during subsequent iterations. This strategy treats your API as first-class citizen and helps you maintaining this API with any upcoming changes!&lt;/p&gt;

&lt;p&gt;I’d love to hear your thoughts or experiences with this approach. Please share your feedback or any questions you might have.&lt;/p&gt;

</description>
      <category>api</category>
      <category>php</category>
      <category>openapi</category>
      <category>flightphp</category>
    </item>
    <item>
      <title>Android XML Resources: Automated Translations with doloc</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Sat, 25 Jan 2025 18:21:43 +0000</pubDate>
      <link>https://dev.to/danielsc/integration-for-android-automated-translations-with-doloc-267i</link>
      <guid>https://dev.to/danielsc/integration-for-android-automated-translations-with-doloc-267i</guid>
      <description>&lt;p&gt;Developing multilingual apps efficiently can be a real challenge - especially when working with various frameworks and tools. This is where doloc comes into play: a lightweight solution for automated translations that integrates seamlessly into your existing workflows. The focus is on great translations and no need for a glossary, as all texts automatically serve implicitly as a glossary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today, we’re excited to introduce our latest integration for Android!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this article, we’ll show you how to use doloc to automate translations in your Android project and save valuable time. It takes you only two steps before you can extract and translate your texts automatically!&lt;/p&gt;

&lt;h2&gt;
  
  
  Android: Automating Translations with doloc
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Set up Andoroid Localization with the &lt;a href="https://developer.android.com/guide/topics/resources/localization" rel="noopener noreferrer"&gt;Andorid documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Add doloc to your workflow by adding the following to your Gradle scripts. Use Groovy or Kotlin DSL depending on your project setup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Groovy DSL (build.gradle):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.net.URI&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.net.http.HttpClient&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.net.http.HttpRequest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.net.http.HttpResponse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.nio.file.Files&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.nio.file.Path&lt;/span&gt;

&lt;span class="c1"&gt;// other existing plugins/tasks..&lt;/span&gt;

&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="n"&gt;targetLang&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;sourceFile&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;targetFile&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;boundary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Boundary-${System.currentTimeMillis()}"&lt;/span&gt;
  &lt;span class="n"&gt;HttpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newHttpClient&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;HttpRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://api.doloc.io?targetLang=$targetLang"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"multipart/form-data; boundary=$boundary"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Bearer ${System.getenv("&lt;/span&gt;&lt;span class="n"&gt;API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;")}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;HttpRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BodyPublishers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;"--$boundary\r\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="s2"&gt;"Content-Disposition: form-data; name=\"source\"; filename=\"${sourceFile.fileName}\"\r\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="s2"&gt;"Content-Type: application/octet-stream\r\n\r\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="s2"&gt;"${Files.readString(sourceFile)}\r\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="s2"&gt;"--$boundary\r\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="s2"&gt;"Content-Disposition: form-data; name=\"target\"; filename=\"${targetFile.fileName}\"\r\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="s2"&gt;"Content-Type: application/octet-stream\r\n\r\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="s2"&gt;"${Files.readString(targetFile)}\r\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="s2"&gt;"--$boundary--\r\n"&lt;/span&gt;
        &lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;HttpResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BodyHandlers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetFile&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"doloc-fr"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;doLast&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;translate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;"fr"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app/src/main/res/values/strings.xml"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;toPath&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
      &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app/src/main/res/values-fr/strings.xml"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;toPath&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"doloc-es"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;doLast&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;translate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;"es"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app/src/main/res/values/strings.xml"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;toPath&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
      &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app/src/main/res/values-es/strings.xml"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;toPath&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"update-i18n"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;dependsOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"doloc-fr"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"doloc-es"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Kotlin DSL (build.gradle.kts):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.net.URI&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.net.http.HttpClient&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.net.http.HttpRequest&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.net.http.HttpResponse&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.nio.file.Files&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.nio.file.Path&lt;/span&gt;

&lt;span class="c1"&gt;// other existing plugins/tasks..&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetLang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sourceFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;boundary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Boundary-${System.currentTimeMillis()}"&lt;/span&gt;
  &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newHttpClient&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.doloc.io?targetLang=$targetLang"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"multipart/form-data; boundary=$boundary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer ${System.getenv("&lt;/span&gt;&lt;span class="nc"&gt;API_TOKEN&lt;/span&gt;&lt;span class="s"&gt;")}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BodyPublishers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ofString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s"&gt;"--$boundary\r\n"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"Content-Disposition: form-data; name=\"source\"; filename=\"${sourceFile.fileName}\"\r\n"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"Content-Type: application/octet-stream\r\n\r\n"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"${Files.readString(sourceFile)}\r\n"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"--$boundary\r\n"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"Content-Disposition: form-data; name=\"target\"; filename=\"${targetFile.fileName}\"\r\n"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"Content-Type: application/octet-stream\r\n\r\n"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"${Files.readString(targetFile)}\r\n"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
          &lt;span class="s"&gt;"--$boundary--\r\n"&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="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BodyHandlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ofFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetFile&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="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"doloc-fr"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;doLast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s"&gt;"fr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app/src/main/res/values/strings.xml"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toPath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app/src/main/res/values-fr/strings.xml"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toPath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"doloc-es"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;doLast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s"&gt;"es"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app/src/main/res/values/strings.xml"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toPath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app/src/main/res/values-es/strings.xml"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toPath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"update-i18n"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;dependsOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"doloc-fr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"doloc-es"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Make sure to update the translation file paths and to replace &lt;code&gt;${System.getenv("API_TOKEN")}&lt;/code&gt; with your API token or set a corresponding environment variable. &lt;/li&gt;
&lt;li&gt;The &lt;code&gt;$API_TOKEN&lt;/code&gt; can be found in your &lt;a href="https://doloc.io/account/" rel="noopener noreferrer"&gt;doloc account&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt; run &lt;code&gt;gradle update-i18n&lt;/code&gt; to translate all new texts into all target languages!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should the skript be run by the developer?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This script should be run &lt;u&gt;after adding or updating translations&lt;/u&gt; and not on the CI/CD pipeline. This has the advantage that the code always stays in sync with the translations and that the developer can check the translations before committing and/or merging them. (Furthermore, this simplifies the CI/CD pipeline and prevents unnecessary commits for translations only.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to handle updated texts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When changing texts in the source file, one should remove the corresponding texts in the target file to ensure that the translations are updated. Alternatively, one can manually update the translations in the target file, of course.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More Info&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To find more info and details to integrate doloc into your Andorid workflow follow our Guide: &lt;a href="https://doloc.io/getting-started/frameworks/android/" rel="noopener noreferrer"&gt;https://doloc.io/getting-started/frameworks/android/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also we provide detailed information on configuration of specific options in the URL in our &lt;a href="https://doloc.io/getting-started/formats/android-xml/" rel="noopener noreferrer"&gt;Android XML Resources&lt;/a&gt; guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By using the Android integration of doloc you have the following advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy integration into the development workflow&lt;/li&gt;
&lt;li&gt;Instant translations of texts via API&lt;/li&gt;
&lt;li&gt;Consistent style without a dictionary&lt;/li&gt;
&lt;li&gt;Reduced translator workload&lt;/li&gt;
&lt;li&gt;Accelerated time to market&lt;/li&gt;
&lt;li&gt;And, of course: Great translations!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leave a comment or send us a message in case you need help or you have questions!&lt;/p&gt;

&lt;p&gt;P.S. This also works for many other formats and frameworks! &lt;br&gt;
Be sure to check out our guides for &lt;a href="https://doloc.io/getting-started/frameworks/angular/" rel="noopener noreferrer"&gt;Angular &lt;/a&gt; and &lt;a href="https://doloc.io/getting-started/frameworks/react-intl-format-js/" rel="noopener noreferrer"&gt;FormatJS/react-intl&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>androiddev</category>
      <category>localization</category>
      <category>appdev</category>
      <category>android</category>
    </item>
    <item>
      <title>Integration for FormatJS/react-intl: Automated Translations with doloc</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Sat, 11 Jan 2025 18:59:51 +0000</pubDate>
      <link>https://dev.to/danielsc/integration-for-formatjsreact-intl-automated-translations-with-doloc-4gpf</link>
      <guid>https://dev.to/danielsc/integration-for-formatjsreact-intl-automated-translations-with-doloc-4gpf</guid>
      <description>&lt;p&gt;Developing multilingual apps efficiently can be a real challenge - especially when working with various frameworks and tools. This is where &lt;a href="https://doloc.io/" rel="noopener noreferrer"&gt;doloc&lt;/a&gt; comes into play: a lightweight solution for automated translations that integrates seamlessly into your existing workflows. The focus is on great translations and no need for a glossary, as all texts automatically serve implicitly as a glossary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today, we’re excited to introduce our latest integration for FormatJS/react-intl!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this article, we’ll show you how to use doloc to automate translations in your FormatJS/react-intl project and save valuable time. It takes you only two steps before you can extract and translate your texts automatically!&lt;/p&gt;

&lt;h2&gt;
  
  
  FormatJS/react-intl: Automating Translations with doloc
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Set up react-intl with the &lt;a href="https://formatjs.github.io/docs/getting-started/installation/" rel="noopener noreferrer"&gt;offical FormatJS guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Add doloc to your workflow by adding the following to your package.json script:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "scripts": {
    // other scripts ...
    "extract": "formatjs extract \"src/**/*.ts*\" --ignore=\"**/*.d.ts\" --out-file src/lang/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
    "doloc-fr": "curl https://api.doloc.io -H \"Authorization: Bearer $API_TOKEN\" -F source=\"@src/lang/en.json\" -F target=\"@src/lang/fr.json\" -o src/lang/fr.json",
    "doloc-es": "curl https://api.doloc.io -H \"Authorization: Bearer $API_TOKEN\" -F source=\"@src/lang/es.json\" -F target=\"@src/lang/es.json\" -o src/lang/es.json",
    "format-js-compile-en": "formatjs compile src/lang/en.json --ast --out-file src/compiled-lang/en.json",
    "format-js-compile-fr": "formatjs compile src/lang/fr.json --ast --out-file src/compiled-lang/fr.json",
    "format-js-compile-es": "formatjs compile src/lang/es.json --ast --out-file src/compiled-lang/es.json",
    "update-i18n": "npm run extract &amp;amp;&amp;amp; npm run doloc-fr &amp;amp;&amp;amp; npm run doloc-es &amp;amp;&amp;amp; npm run format-js-compile-en &amp;amp;&amp;amp; npm run format-js-compile-fr &amp;amp;&amp;amp; npm run format-js-compile-es"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;$API_TOKEN&lt;/code&gt; can be found in your &lt;a href="https://doloc.io/account/" rel="noopener noreferrer"&gt;doloc account&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt; run &lt;code&gt;npm run update-i18n&lt;/code&gt; to extract new translations and automatically translate them.&lt;/p&gt;

&lt;p&gt;To find more infos and details for configuration follow our guide on &lt;a href="https://doloc.io/getting-started/frameworks/react-intl-format-js/" rel="noopener noreferrer"&gt;https://doloc.io/getting-started/frameworks/react-intl-format-js/&lt;/a&gt;.&lt;br&gt;
There you'll also find information on how to handle explicit IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By using the FormatJS/react-intl integration of doloc you have the following advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy integration into the development workflow&lt;/li&gt;
&lt;li&gt;Instant translations of texts via API&lt;/li&gt;
&lt;li&gt;Consistent style without a dictionary&lt;/li&gt;
&lt;li&gt;Reduced translator workload&lt;/li&gt;
&lt;li&gt;Accelerated time to market&lt;/li&gt;
&lt;li&gt;And, of course: Great translations!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leave a comment or send us a message in case you need help or you have questions!&lt;/p&gt;

&lt;p&gt;P.S. This also works for many other formats and frameworks!&lt;br&gt;
Be sure to check out our guides for &lt;a href="https://doloc.io/getting-started/frameworks/android/" rel="noopener noreferrer"&gt;Android&lt;/a&gt; and &lt;a href="https://doloc.io/getting-started/frameworks/angular/" rel="noopener noreferrer"&gt;Angular&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>localization</category>
      <category>i18n</category>
      <category>json</category>
    </item>
    <item>
      <title>Integration for Angular: Automated Translations with doloc</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Sat, 11 Jan 2025 18:59:36 +0000</pubDate>
      <link>https://dev.to/danielsc/integration-for-angular-automated-translations-with-doloc-45mn</link>
      <guid>https://dev.to/danielsc/integration-for-angular-automated-translations-with-doloc-45mn</guid>
      <description>&lt;p&gt;Developing multilingual apps efficiently can be a real challenge - especially when working with various frameworks and tools. This is where &lt;a href="https://doloc.io/" rel="noopener noreferrer"&gt;doloc&lt;/a&gt; comes into play: a lightweight solution for automated translations that integrates seamlessly into your existing workflows. The focus is on great translations and no need for a glossary, as all texts automatically serve implicitly as a glossary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today, we’re excited to introduce our latest integration for Angular!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this article, we’ll show you how to use doloc to automate translations in your Angular project and save valuable time. It takes you only three steps before you can extract and translate your texts automatically!&lt;/p&gt;

&lt;h2&gt;
  
  
  Angular: Automating Translations with doloc
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Set up Angular Localization &lt;code&gt;i18n&lt;/code&gt; with the &lt;a href="https://angular.dev/guide/i18n" rel="noopener noreferrer"&gt;official Angular guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Set up &lt;a href="https://github.com/daniel-sc/ng-extract-i18n-merge" rel="noopener noreferrer"&gt;&lt;code&gt;ng-extract-i18n-merge&lt;/code&gt;&lt;/a&gt; to manage translations.&lt;/li&gt;
&lt;li&gt;Add doloc to your workflow by adding the following to your package.json script:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "scripts": {
    // other scripts ...
    "auto-translate-fr": "curl https://api.doloc.io -H \"Authorization: Bearer $API_TOKEN\" --data-binary @src/messages.fr.xlf -o src/messages.fr.xlf",
    "auto-translate-it": "curl https://api.doloc.io -H \"Authorization: Bearer $API_TOKEN\" --data-binary @src/messages.it.xlf -o src/messages.it.xlf",
    "update-i18n": "ng extract-i18n &amp;amp;&amp;amp; npm run auto-translate-fr &amp;amp;&amp;amp; npm run auto-translate-it"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;$API_TOKEN&lt;/code&gt; can be found in your &lt;a href="https://doloc.io/account/" rel="noopener noreferrer"&gt;doloc account&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt; run &lt;code&gt;npm run update-i18n&lt;/code&gt; to extract new translations and automatically translate them.&lt;/p&gt;

&lt;p&gt;To find more info and details for configuration follow our guide on &lt;a href="https://doloc.io/getting-started/frameworks/angular/" rel="noopener noreferrer"&gt;https://doloc.io/getting-started/frameworks/angular/&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By using the Angular integration of doloc you have the following advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy integration into the development workflow&lt;/li&gt;
&lt;li&gt;Instant translations of texts via API&lt;/li&gt;
&lt;li&gt;Consistent style without a dictionary&lt;/li&gt;
&lt;li&gt;Reduced translator workload&lt;/li&gt;
&lt;li&gt;Accelerated time to market&lt;/li&gt;
&lt;li&gt;And, of course: Great translations!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leave a comment or send us a message in case you need help or you have questions!&lt;/p&gt;

&lt;p&gt;P.S. This also works for many other formats and frameworks!&lt;br&gt;
Be sure to check out our guides for &lt;a href="https://doloc.io/getting-started/frameworks/android/" rel="noopener noreferrer"&gt;Android&lt;/a&gt; and &lt;a href="https://doloc.io/getting-started/frameworks/react-intl-format-js/" rel="noopener noreferrer"&gt;FormatJS/react-intl&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>localization</category>
      <category>i18n</category>
      <category>xliff</category>
    </item>
    <item>
      <title>App Localization with doloc</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Mon, 11 Nov 2024 21:27:35 +0000</pubDate>
      <link>https://dev.to/danielsc/streamline-app-localization-with-doloc-2k2c</link>
      <guid>https://dev.to/danielsc/streamline-app-localization-with-doloc-2k2c</guid>
      <description>&lt;p&gt;In this post, I'll outline an optimized translation workflow for app localization&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. This approach applies to both web and native applications (iOS, Android, Windows, etc.), with just one prerequisite: using XLIFF (1.2 or 2.0) for your translation files. XLIFF is widely supported across localization tools, making it a practical choice for most teams.&lt;/p&gt;

&lt;p&gt;The proposed setup can dramatically reduce time-to-market and lower translation costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Traditional Workflow
&lt;/h2&gt;

&lt;p&gt;To understand where bottlenecks and delays arise, let’s take a quick look at the traditional localization workflow:&lt;/p&gt;

&lt;p&gt;Typically, translations are handled by third parties rather than developers themselves. This might be an agency, an in-house service, or another individual we’ll refer to as the "translator."&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Extraction&lt;/strong&gt;: The developer extracts the app's text into translation files (XLIFF).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Handover&lt;/strong&gt;: The extracted files are handed over to the translator, usually in batches. This batching contributes significantly to delays; for example, new texts may only be sent for translation every two weeks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Translation&lt;/strong&gt;: The translator works on these files, translating new or updated text and striving to maintain language consistency. This step, often taking 1-3 days, requires effort and is prone to errors, particularly in larger apps with limited tooling support.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Integration&lt;/strong&gt;: Once translations are complete, the files are handed back to the developer to integrate into the codebase. With the delay, this can involve resolving complex conflicts, adding manual effort to the process.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;In summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Localization Delays&lt;/strong&gt;: Typically, there’s a period of two weeks or more where the app isn’t fully localized, which can block releases or degrade the user experience for non-default languages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual and Error-Prone Steps&lt;/strong&gt;: The manual handover and integration steps increase costs and the likelihood of errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Leaner Workflow with Automated Translations
&lt;/h2&gt;

&lt;p&gt;By integrating &lt;a href="https://doloc.io" rel="noopener noreferrer"&gt;doloc&lt;/a&gt; into the extraction process, you can streamline this workflow significantly. Here’s how it works:&lt;/p&gt;

&lt;p&gt;Instead of a separate translation step, the extracted XLIFF files are sent to doloc automatically (via a simple &lt;code&gt;curl&lt;/code&gt; command - see &lt;a href="https://doloc.io/getting-started/" rel="noopener noreferrer"&gt;getting started&lt;/a&gt;. Doloc immediately translates the content and returns the updated files. The entire process boils down to just one step (plus an optional QA step):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Extraction and Translation&lt;/strong&gt;: The developer extracts the text into XLIFF files, and doloc instantly translates them. The files are updated right away, so translations are available from the start.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Optional: Quality Assurance (QA)&lt;/strong&gt;: If desired, a translator can quickly validate automated translations. XLIFF includes specific states (e.g., "translated," "final") to mark reviewed segments.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This automated setup means QA efforts are minimal, and releases are no longer delayed by translation. Since your app is always localized, you may even find yourself reducing QA cycles as you gain confidence in the quality of doloc’s translations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Localization&lt;/strong&gt;: Translations are always up to date, ensuring a fully localized experience for users across languages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrated Testing&lt;/strong&gt;: Localized text can be included in your acceptance tests, helping you catch potential issues early and maintain overall quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  We Value Your Feedback
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: I am the founder of doloc.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We're always working to improve. If you've tried doloc (we offer a free &lt;a href="https://doloc.io/pricing" rel="noopener noreferrer"&gt;14-day trial&lt;/a&gt;!) or if you have thoughts on whether it might fit into your workflow, we’d love to hear from you. Drop a comment below!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;em&gt;Localization&lt;/em&gt; includes more than just &lt;em&gt;translation&lt;/em&gt;, but since other aspects are often centrally controlled or configured, we focus here on translation as the primary challenge. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>xliff</category>
      <category>localization</category>
      <category>i18n</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Client side (temporary) IDs</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Thu, 21 Apr 2022 15:34:25 +0000</pubDate>
      <link>https://dev.to/danielsc/client-side-temporary-ids-5c2k</link>
      <guid>https://dev.to/danielsc/client-side-temporary-ids-5c2k</guid>
      <description>&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;Modern UIs often implement offline capabilities and/or follow the &lt;a href="https://uxplanet.org/optimistic-1000-34d9eefe4c05" rel="noopener noreferrer"&gt;optimistic ui&lt;/a&gt; pattern. In these cases you'd need to handle new entities in your client side state &lt;em&gt;before&lt;/em&gt; a server response (which traditionally contains the ID) is available. To support this, it is helpful to create IDs on the client side.&lt;/p&gt;

&lt;p&gt;When you create IDs client side, you could follow different setups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Traditional Server&lt;/strong&gt; Server does not know about any client side IDs. Client "merges" server generated IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client only&lt;/strong&gt; Server does not generate IDs but expects (all) clients to provide IDs. Normally used with UUIDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server side mapping&lt;/strong&gt; Clients propagate their generated ID to the server, but the server generates new ID and keeps/creates (a mapping of) both IDs (e.g. adding an attribute &lt;code&gt;client_id&lt;/code&gt; to the entity).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even though all setups are sensible (depending on the specific context), I'd like to highlight that only "Traditional Server" can be used for scenarios where you have no control over the server/API (or do not want to change these).&lt;/p&gt;

&lt;p&gt;The following sections will focus on the &lt;strong&gt;Traditional Server&lt;/strong&gt; setup and document some best practices. Please reach out if something seems incorrect or missing!&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep temporary ID around
&lt;/h2&gt;

&lt;p&gt;Even after the server supplied the (persistent) ID, it is often helpful to keep the corresponding temporary ID around - at least until the entity is removed from the client state.&lt;/p&gt;

&lt;p&gt;For example this is useful when tracking list/table entries (e.g. with a &lt;code&gt;trackBy&lt;/code&gt; function in Angular):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;trackBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temporaryId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above can only continuously identify the object, if you do &lt;em&gt;not&lt;/em&gt; remove the &lt;code&gt;temporaryId&lt;/code&gt; (even once the &lt;code&gt;id&lt;/code&gt; is present).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you use the temporary ID in your URL, you should replace the URL with the persistent ID as soon as possible to make sure on a page refresh (when client state is generally reset) it is possible to re-fetch the entity from the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a separate attribute
&lt;/h2&gt;

&lt;p&gt;You can put the client generated ID into the same attribute as the server generated ID. In this case it is advisable to clearly distinguish client/local IDs and server IDs - e.g. by prefix or by using negative numbers.&lt;/p&gt;

&lt;p&gt;Generally it is easier to maintain your code if the client/local ID is put into a separate attribute - e.g. &lt;code&gt;temporaryId&lt;/code&gt;. Not only does this enable to "Keep temporary ID around" (see above), but it is also less error prone, as you cannot mistakenly mix up client and server IDs.&lt;/p&gt;

&lt;p&gt;In Typescript you can easily define an interface&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;HasTempId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;temporaryId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and join this into your models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;YourEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;HasTempId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* whatever */&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it easy, to retrospectively add client side generated IDs to already existing code.&lt;/p&gt;

&lt;p&gt;Admittingly, this is not as easy for &lt;a href="https://medium.com/@thejameskyle/type-systems-structural-vs-nominal-typing-explained-56511dd969f4" rel="noopener noreferrer"&gt;nominally typed languages&lt;/a&gt; such as Java.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referential Consistency
&lt;/h2&gt;

&lt;p&gt;When entities reference other entities, these references should be taken into account. In general it is not necessary to create additional properties to hold temporary references, but you can first put the temporary/client ID into the original reference attribute and replace it with the persistent ID, once it is available. Of course, you should take care that all references of all relevant entities are updated.&lt;/p&gt;

&lt;p&gt;This works naturally with central state patterns, such as Redux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stable order
&lt;/h2&gt;

&lt;p&gt;If you have sub-resources modeled as a list that have IDs on their own (UML "Aggregation") you need to match all elements. To correctly associate client side IDs with server side IDs, the easiest way is to guarantee the order of this list (server) and just associate elements by their index.&lt;/p&gt;

&lt;p&gt;In case the former is not possible you can still identify elements by their structure/values. Note that if you cannot distinguish elements, the correct order does not matter.&lt;/p&gt;

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

&lt;p&gt;Generating and maintaining client side temporary IDs does add some complexity. A structured approach and using a modern (Web) stack with Typescript and a centralized state management helps keeping the solution maintainable.&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Angular i18n update workflow</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Thu, 20 Jan 2022 14:21:59 +0000</pubDate>
      <link>https://dev.to/danielsc/angular-i18n-update-workflow-25p6</link>
      <guid>https://dev.to/danielsc/angular-i18n-update-workflow-25p6</guid>
      <description>&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Reading this, you are probably already aware, that tooling for updating &lt;a href="https://angular.io/guide/i18n-overview" rel="noopener noreferrer"&gt;internationalization files in angular&lt;/a&gt; is limited (non-existent). You can easily extract translation texts from your templates with the &lt;code&gt;ng extract-i18n&lt;/code&gt; command - but it is not clear, how to merge new/removed/changed texts into already translated language files.&lt;/p&gt;

&lt;p&gt;The core team does not see it as their responsibility - see &lt;a href="https://github.com/angular/angular/issues/37655" rel="noopener noreferrer"&gt;angular/angular/issues/37655&lt;/a&gt; and &lt;a href="https://github.com/angular/angular-cli/issues/6552" rel="noopener noreferrer"&gt;angular/angular-cli/issues/6552&lt;/a&gt;. There existed some tooling &lt;a href="https://github.com/martinroob/ngx-i18nsupport" rel="noopener noreferrer"&gt;@ngx-i18nsupport&lt;/a&gt; on which the community relied, but sadly this is unmaintained and broken (and probably too complex for "fork and repair"...).&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;




&lt;h3&gt;
  
  
  Update
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;As the below solution is a little cumbersome, I created a plugin that nicely integrates with Angular CLI: &lt;a href="https://github.com/daniel-sc/ng-extract-i18n-merge" rel="noopener noreferrer"&gt;https://github.com/daniel-sc/ng-extract-i18n-merge&lt;/a&gt;&lt;br&gt;
With that setup comes down to&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng add ng-extract-i18n-merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;and extraction and merging gets as simple as&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng run &lt;span class="o"&gt;[&lt;/span&gt;PROJECT_ID]:extract-i18n-merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or (if you confirmed to adding an npm command):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run extract-i18n-merge 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;I wrote two small tools, that solve the i18n merge problem: &lt;a href="https://github.com/daniel-sc/xliff-simple-merge" rel="noopener noreferrer"&gt;xliff-simple-merge&lt;/a&gt; and &lt;a href="https://github.com/daniel-sc/xml_normalize" rel="noopener noreferrer"&gt;xml_normalize&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The following example setup illustrates how you can extract, merge and normalize (remove "notes", sort by ID, pretty print) translations for your angular app with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run i18n-extract
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup to be included in "scripts" of &lt;code&gt;package.json&lt;/code&gt; (assuming you use XLIFF 2.0 format):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"i18n-extract"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ng extract-i18n --format xlf2 --output-path src/i18n &amp;amp;&amp;amp; npm run normalize-xliff-base &amp;amp;&amp;amp; npm run merge-xliff-all &amp;amp;&amp;amp; npm run normalize-xliff-all"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"merge-xliff-all"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run merge-xliff-de &amp;amp;&amp;amp; npm run merge-xliff-fr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"merge-xliff-de"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node node_modules/xliff-simple-merge -i src/i18n/messages.xlf -d src/i18n/messages.de.xlf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"merge-xliff-fr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node node_modules/xliff-simple-merge -i src/i18n/messages.xlf -d src/i18n/messages.fr.xlf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"normalize-xliff-all"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run normalize-xliff-base &amp;amp;&amp;amp; npm run normalize-xliff-de &amp;amp;&amp;amp; npm run normalize-xliff-fr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"normalize-xliff-base"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node node_modules/xml_normalize -n -i src/i18n/messages.xlf -o src/i18n/messages.xlf -r /xliff/file/unit/notes -s /xliff/file/unit/@id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"normalize-xliff-de"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node node_modules/xml_normalize -n -i src/i18n/messages.de.xlf -o src/i18n/messages.de.xlf -r /xliff/file/unit/notes -s /xliff/file/unit/@id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"normalize-xliff-fr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node node_modules/xml_normalize -n -i src/i18n/messages.fr.xlf -o src/i18n/messages.fr.xlf -r /xliff/file/unit/notes -s /xliff/file/unit/@id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"xliff-simple-merge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"xml_normalize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.8.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Other work
&lt;/h3&gt;

&lt;p&gt;If you (as the developer) do the translations yourself, you might find a more integrated solution, like &lt;a href="https://github.com/kyubisation/angular-t9n" rel="noopener noreferrer"&gt;angular-t9n&lt;/a&gt;, better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback welcome!
&lt;/h2&gt;

&lt;p&gt;If you have a better setup or suggestions for improvement I'd be happy, if you leave a comment :-)&lt;/p&gt;

</description>
      <category>angular</category>
      <category>i18n</category>
    </item>
    <item>
      <title>Testing STOMP websocket with Postman</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Thu, 12 Aug 2021 12:23:21 +0000</pubDate>
      <link>https://dev.to/danielsc/testing-stomp-websocket-with-postman-218a</link>
      <guid>https://dev.to/danielsc/testing-stomp-websocket-with-postman-218a</guid>
      <description>&lt;p&gt;With Postman v8 you can test Websockets - great! But..&lt;/p&gt;

&lt;p&gt;When testing &lt;a href="https://stomp.github.io/" rel="noopener noreferrer"&gt;STOMP&lt;/a&gt; servers you will encounter a problem: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The body is then followed by the NULL octet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;See &lt;a href="https://stomp.github.io/stomp-specification-1.2.html#STOMP_Frames" rel="noopener noreferrer"&gt;https://stomp.github.io/stomp-specification-1.2.html#STOMP_Frames&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The only way (on windows) to get the NULL octet into the frame is by using the "binary/Base64" encoding (see screenshot below).&lt;/p&gt;

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

&lt;p&gt;To easily convert the frame you can use &lt;a href="https://notepad-plus-plus.org/" rel="noopener noreferrer"&gt;Notepad++&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Activate "Character Panel"&lt;/li&gt;
&lt;li&gt;Add "NULL" at the end of the frame (after all headers and body) - make sure to actually click on "NULL"&lt;/li&gt;
&lt;li&gt;Select complete frame and encode it to Base64&lt;/li&gt;
&lt;/ol&gt;

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

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

&lt;p&gt;If you found an easier way, please comment!&lt;/p&gt;

</description>
      <category>stomp</category>
      <category>postman</category>
      <category>testing</category>
    </item>
    <item>
      <title>Firebase function retries with pubsub</title>
      <dc:creator>Daniel Schreiber</dc:creator>
      <pubDate>Thu, 11 Feb 2021 09:10:30 +0000</pubDate>
      <link>https://dev.to/danielsc/firebase-function-retries-with-pubsub-3jf9</link>
      <guid>https://dev.to/danielsc/firebase-function-retries-with-pubsub-3jf9</guid>
      <description>&lt;p&gt;Ever tried to use exponential backoff for pubsub triggered firebase functions? It’s not straight forward, but I’ll walk you through. &lt;/p&gt;

&lt;h1&gt;
  
  
  Why?
&lt;/h1&gt;

&lt;p&gt;Decoupling cloud/firebase functions from the trigger/invocation via pubsub message topics is a best practice to improve reliability and resilience. Especially for the latter you’d want to have a sensible &lt;a href="https://firebase.google.com/docs/functions/retries" rel="noopener noreferrer"&gt;retry setup&lt;/a&gt;, so that transient errors are retried. This retry can be configured in the pubsub subscription that triggers the function. &lt;/p&gt;

&lt;h1&gt;
  
  
  What is the problem?
&lt;/h1&gt;

&lt;p&gt;Consider this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;region&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;europe-west1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runWith&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;failurePolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;retry&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;pubsub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-topic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onPublish&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When deploying the function using the firebase cli, the subscription is recreated with standard retry settings. Using the firebase cli, it is currently not possible to define exponential backoff for the retry. &lt;/p&gt;

&lt;p&gt;Note that the &lt;em&gt;default settings can have a severe impact on your cloud bill&lt;/em&gt;. When retries are enabled (&lt;code&gt;failurePolicy: {retry: {}}&lt;/code&gt;), the standard configuration retries immediately and for 7 days! So for non-transient errors or problems with a longer recovery time, you’ll pay for &lt;em&gt;many&lt;/em&gt; invitations..&lt;/p&gt;

&lt;h1&gt;
  
  
  What is the solution?
&lt;/h1&gt;

&lt;p&gt;After each function deployment, you need to update the subscription accordingly. The following configures retries with exponential backoff for at most 2 hours and delays between 10 seconds and 10 minutes:&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;# regular deployment:&lt;/span&gt;
firebase deploy &lt;span class="nt"&gt;--only&lt;/span&gt; functions &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;span class="c"&gt;# update pubsub subscription for retries with exponential backoff:&lt;/span&gt;
gcloud pubsub subscriptions update &lt;span class="se"&gt;\&lt;/span&gt;
 projects/&lt;span class="nv"&gt;$GCP_PROJECT_ID&lt;/span&gt;/subscriptions/gcf-myFunction-europe-west1-my-topic &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--min-retry-delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10s &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--max-retry-delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10m &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--message-retention-duration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;--force&lt;/code&gt; is needed in CI setup to deploy functions with retries. &lt;/p&gt;

&lt;h1&gt;
  
  
  What are the alternatives?
&lt;/h1&gt;

&lt;p&gt;You could as well &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;manually update the subscription (not recommend)&lt;/li&gt;
&lt;li&gt;move from firebase cli to a direct deployment on GCP (eg via terraform)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;If I missed something or you have other solutions, please comment!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>pubsub</category>
      <category>gcp</category>
    </item>
  </channel>
</rss>
