<?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: Devansh Choudhary</title>
    <description>The latest articles on DEV Community by Devansh Choudhary (@devansh_choudhary_797aa6c).</description>
    <link>https://dev.to/devansh_choudhary_797aa6c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4176951%2F59e6547f-8633-42b6-a74c-648292a27ac7.jpg</url>
      <title>DEV Community: Devansh Choudhary</title>
      <link>https://dev.to/devansh_choudhary_797aa6c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devansh_choudhary_797aa6c"/>
    <language>en</language>
    <item>
      <title>How I read an Etsy order export in the browser without uploading it</title>
      <dc:creator>Devansh Choudhary</dc:creator>
      <pubDate>Sun, 11 Oct 2026 12:43:10 +0000</pubDate>
      <link>https://dev.to/devansh_choudhary_797aa6c/how-i-read-an-etsy-order-export-in-the-browser-without-uploading-it-221o</link>
      <guid>https://dev.to/devansh_choudhary_797aa6c/how-i-read-an-etsy-order-export-in-the-browser-without-uploading-it-221o</guid>
      <description>&lt;p&gt;A new EU packaging rule made Etsy ask some sellers for an EPR number. The sellers' question was how much of their shop that touches. The answer sits in one CSV, and that CSV is full of other people's addresses. This post covers how I read it without sending it anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the export looks like
&lt;/h2&gt;

&lt;p&gt;Etsy's Order Items file has one row per item sold. The header includes &lt;code&gt;Buyer&lt;/code&gt;, &lt;code&gt;Ship Name&lt;/code&gt;, &lt;code&gt;Ship Address1&lt;/code&gt;, &lt;code&gt;Ship City&lt;/code&gt; and &lt;code&gt;Ship Zipcode&lt;/code&gt;. So the file holds your buyers' names and home addresses.&lt;/p&gt;

&lt;p&gt;Column order varies between years. I match columns by header name, not by position, and a test shuffles the columns to keep that true. The lookup is a map from normalised header to the real header:&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;keys&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;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;headerKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;field&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;has&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;isItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Transaction ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Listing ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another test pins a real 2025 export. If someone drops the Orders file instead, the page says which Shop Manager path leads to the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing in the browser
&lt;/h2&gt;

&lt;p&gt;The page hands the &lt;code&gt;File&lt;/code&gt; object to papaparse. Without a &lt;code&gt;download&lt;/code&gt; option, papaparse reads it through a FileReader, so the bytes never become a request. The only &lt;code&gt;fetch&lt;/code&gt; in the source is a counter ping. Its body is &lt;code&gt;{event, source}&lt;/code&gt;, plus one optional typed answer on a &lt;code&gt;switch&lt;/code&gt; event, capped at 200 characters. The Worker (&lt;code&gt;worker/index.ts&lt;/code&gt;) rejects any other field. I probed it with an extra &lt;code&gt;country&lt;/code&gt; field and got a 400.&lt;/p&gt;

&lt;p&gt;You can check this in the Network panel. Load the page, then drop a file. You will see two POSTs to the counter, &lt;code&gt;view&lt;/code&gt; and &lt;code&gt;parsed&lt;/code&gt;, at most 74 bytes each. The page also loads its own assets and may send a Cloudflare network error report. The claim I make is narrower: the contents of the file never leave the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parcel rule
&lt;/h2&gt;

&lt;p&gt;An order with three items ships as one parcel. So packaging weights need two kinds of parts. Per-item parts, such as a filler pad, multiply by quantity. Per-order parts, such as the box, count once, and for a mixed order the biggest box wins. Orders are counted by distinct Order ID, not by rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  One currency at a time
&lt;/h2&gt;

&lt;p&gt;A shop that sells in USD, GBP and EUR cannot add the totals without choosing a rate. I did not choose one. Item sales stay in their own currency per country. The sum is a record keyed by currency code:&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;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test named "counts distinct orders and keeps currencies apart" holds that line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not work
&lt;/h2&gt;

&lt;p&gt;My plan described the CSV from second-hand sources: a vendor blog and two GitHub repos, one of them a hand-written test fixture. That plan was wrong twice. It had money with a currency symbol (&lt;code&gt;$5.00&lt;/code&gt;) and order IDs with a &lt;code&gt;#&lt;/code&gt;. Real exports have neither, and &lt;code&gt;Date Shipped&lt;/code&gt; is &lt;code&gt;MM/DD/YYYY&lt;/code&gt; while &lt;code&gt;Sale Date&lt;/code&gt; is &lt;code&gt;MM/DD/YY&lt;/code&gt;. I found this by parsing four real exports from public repos, dated 2017 to 2025. The parser now tolerates a &lt;code&gt;#&lt;/code&gt; and accepts &lt;code&gt;Date Posted&lt;/code&gt; as an alias, and a real 2025 export is pinned as a test fixture. I still have no export in a localised header language, and a file re-saved by Excel in a German locale fails with a message that names the date it cannot read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quoting the registers word for word
&lt;/h2&gt;

&lt;p&gt;Germany, France and Spain each publish a sentence about what a seller must report. I copy each sentence verbatim, with the link and the date I copied it. A script fetches the sources and checks every quote. Eight of eight matched today: four German exact, and the French and Spanish PDF quotes after whitespace removal, because of line breaks. My translations are marked as mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The page
&lt;/h2&gt;

&lt;p&gt;I wrapped this in a free page called eprkilo. You drop the Order Items file and the first screen shows what share of your orders went to the EU, with Germany, France and Spain marked as the countries where Etsy asks for an EPR number. The sample button shows 18.4% (9 of 49) on invented data. It does arithmetic and gives kilograms to three decimals. It does not say whether you must register. The source is public under MIT, and I built it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eprkilo.pages.dev/?utm_source=devto" rel="noopener noreferrer"&gt;https://eprkilo.pages.dev/?utm_source=devto&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
