<?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: Petro Kozlov</title>
    <description>The latest articles on DEV Community by Petro Kozlov (@che1974).</description>
    <link>https://dev.to/che1974</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%2F381345%2Fe1e0df54-8578-43c4-a9e8-1ad908375ebe.png</url>
      <title>DEV Community: Petro Kozlov</title>
      <link>https://dev.to/che1974</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/che1974"/>
    <language>en</language>
    <item>
      <title>I got tired of my messy Downloads folder, so I built an app that organizes documents by reading them</title>
      <dc:creator>Petro Kozlov</dc:creator>
      <pubDate>Mon, 23 Mar 2026 09:10:18 +0000</pubDate>
      <link>https://dev.to/che1974/i-got-tired-of-my-messy-downloads-folder-so-i-built-an-app-that-organizes-documents-by-reading-them-323f</link>
      <guid>https://dev.to/che1974/i-got-tired-of-my-messy-downloads-folder-so-i-built-an-app-that-organizes-documents-by-reading-them-323f</guid>
      <description>&lt;p&gt;Every month I download dozens of PDFs — invoices, contracts, pay slips, bank statements. They all land in Downloads with names like &lt;code&gt;scan_001.pdf&lt;/code&gt;, &lt;code&gt;document(3).pdf&lt;/code&gt;, or my personal favorite: &lt;code&gt;Unbenannt.pdf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I kept telling myself I'd organize them "later." Three years of "later" gave me 2,000+ files in one folder.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why existing tools didn't work for me
&lt;/h2&gt;

&lt;p&gt;I live in Germany, and privacy matters here — a lot. Sending my tax documents, rental contracts, and bank statements to some cloud AI service was not an option. Here's what I tried:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Cowork&lt;/strong&gt; — impressive, but requires a $20+/month subscription, sends data to the cloud, and you have to manually ask it each time. Not "set it and forget it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS Smart Folders / Hazel&lt;/strong&gt; — rule-based, but doesn't understand document &lt;em&gt;content&lt;/em&gt;. Sorting by file extension puts all PDFs in one pile. That's not organizing, that's moving the mess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI File Sorter, Sorted, Sparkle&lt;/strong&gt; — either cloud-dependent, subscription-based, macOS-only, or all three.&lt;/p&gt;

&lt;p&gt;What I wanted was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Watches my folders &lt;strong&gt;automatically&lt;/strong&gt; in the background&lt;/li&gt;
&lt;li&gt;Reads document content and &lt;strong&gt;classifies&lt;/strong&gt; it (invoice, contract, pay slip...)&lt;/li&gt;
&lt;li&gt;Suggests a &lt;strong&gt;rename&lt;/strong&gt; and &lt;strong&gt;target folder&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Runs &lt;strong&gt;100% offline&lt;/strong&gt; — nothing leaves my machine&lt;/li&gt;
&lt;li&gt;Works on &lt;strong&gt;Windows, Mac, and Linux&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet Ablage
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Ablage"&lt;/em&gt; is German for "filing" — the tray on your desk where documents go to be sorted. That's exactly what this app does, but digitally.&lt;/p&gt;

&lt;p&gt;Ablage sits in your system tray and watches folders you choose. When a new file appears, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extracts text&lt;/strong&gt; from PDFs and DOCX files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classifies&lt;/strong&gt; the document type using configurable rules (keywords or regex)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suggests a new name&lt;/strong&gt; like &lt;code&gt;Rechnung_Telekom_2026-03-15.pdf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suggests a target folder&lt;/strong&gt; like &lt;code&gt;Finanzen/Rechnungen/2026/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Shows a &lt;strong&gt;notification&lt;/strong&gt; — you apply, customize, or skip&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No ML models. No neural networks. No 500MB downloads. Just smart pattern matching that understands German documents surprisingly well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "no ML" bet that actually worked
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody talks about: for domain-specific document classification, &lt;strong&gt;regex beats ML in 80% of cases&lt;/strong&gt; if you know your domain.&lt;/p&gt;

&lt;p&gt;German financial documents are incredibly predictable. An invoice &lt;em&gt;always&lt;/em&gt; contains words like "Rechnungsnummer", "MwSt", "Gesamtbetrag". A rental contract &lt;em&gt;always&lt;/em&gt; has "Kündigungsfrist" and "Mietvertrag". A pay slip &lt;em&gt;always&lt;/em&gt; mentions "Bruttolohn" and "Steuerklasse".&lt;/p&gt;

&lt;p&gt;Ablage supports two rule types:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyword rules&lt;/strong&gt; — comma-separated terms matched against document text, with a configurable minimum match threshold. For example, a rule with keywords &lt;code&gt;invoice, total, payment, due date&lt;/code&gt; and min 2 matches will fire when at least two of those terms appear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regex rules&lt;/strong&gt; — full regular expression patterns for precise matching. Something like &lt;code&gt;rechnung.*nr.*\d+&lt;/code&gt; catches invoice numbers regardless of formatting.&lt;/p&gt;

&lt;p&gt;Each rule maps to a document type, a target folder (with &lt;code&gt;{YYYY}&lt;/code&gt; placeholder for year-based organization), and a filename template using &lt;code&gt;{Sender}&lt;/code&gt;, &lt;code&gt;{Date}&lt;/code&gt;, and &lt;code&gt;{ext}&lt;/code&gt; placeholders.&lt;/p&gt;

&lt;p&gt;Seven default rules ship out of the box covering the most common German document types. But the real power is in the rule editor — you can create your own rules through a step-by-step wizard or edit them inline.&lt;/p&gt;

&lt;p&gt;Here's a simplified version of how the classifier works:&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;function&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;ClassificationResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bestMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClassificationResult&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;for &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;rule&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;score&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keywords&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kw&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minMatches&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// regex rule&lt;/span&gt;
      &lt;span class="nx"&gt;score&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;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bestMatch&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bestMatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;bestMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;extractFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;suggestedName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;applyTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nameTemplate&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="na"&gt;suggestedFolder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;applyTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;folder&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="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;return&lt;/span&gt; &lt;span class="nx"&gt;bestMatch&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No training data, no model files, no GPU. And it correctly classifies the vast majority of typical German household documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: keeping it simple
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  main/              # Electron main process
    watcher.ts         # File system monitoring (chokidar)
    extractor.ts       # Text extraction (PDF, DOCX)
    classifier.ts      # Rule-based document classification
    pipeline.ts        # Orchestration: watch → extract → classify → notify
    mover.ts           # File rename + move with undo
    database.ts        # SQLite: rules, history, settings
    tray.ts            # System tray menu
    notifications.ts   # Native + in-app notifications
    ipc-handlers.ts    # IPC bridge to renderer
  renderer/           # React UI
    components/
      Settings.tsx       # Watch folders + language
      RuleEditor.tsx     # Rule list with inline editing
      RuleWizard.tsx     # Step-by-step rule creation
      History.tsx        # Operation log with undo
      Notification.tsx   # In-app suggestion cards
      Onboarding.tsx     # First-launch guide
  shared/
    types.ts           # Shared TypeScript interfaces
    i18n/              # Locale files (en, de, uk, fr)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Electron 41 + React 19 + TypeScript 5&lt;/strong&gt; — cross-platform desktop with system tray&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite + vite-plugin-electron&lt;/strong&gt; — fast dev experience with HMR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;chokidar&lt;/strong&gt; — native file system watcher&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pdf-parse&lt;/strong&gt; — PDF text extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mammoth&lt;/strong&gt; — DOCX text extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;better-sqlite3&lt;/strong&gt; — local database for rules, history, and settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs in Electron's main process — no external services, no API calls, no internet required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The file watcher: harder than it sounds
&lt;/h2&gt;

&lt;p&gt;One thing I underestimated: file watching is full of edge cases.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;awaitWriteFinish&lt;/code&gt; option in chokidar is critical — without it, you'll try to read a PDF that Chrome is still downloading and get corrupted data. A 2-second stability threshold catches most cases.&lt;/p&gt;

&lt;p&gt;Other gotchas I ran into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser downloads&lt;/strong&gt; create a &lt;code&gt;.crdownload&lt;/code&gt; (Chrome) or &lt;code&gt;.part&lt;/code&gt; (Firefox) file first, then rename. You need to ignore the temp file and catch the rename event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File moves within watched folders&lt;/strong&gt; trigger both &lt;code&gt;unlink&lt;/code&gt; and &lt;code&gt;add&lt;/code&gt; events. Without deduplication you'll process the same file twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission errors&lt;/strong&gt; on Windows when antivirus locks a newly created file. Retry with backoff solves this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large files&lt;/strong&gt; — a 50MB PDF takes time to extract. The pipeline runs async so the UI never blocks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Smart renaming: the devil is in the date formats
&lt;/h2&gt;

&lt;p&gt;Germans love dates. But they can't agree on a format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;15.03.2026&lt;/code&gt; (DD.MM.YYYY — most common)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2026-03-15&lt;/code&gt; (ISO — in modern documents)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;15/03/2026&lt;/code&gt; (DD/MM/YYYY — occasionally)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;März 2026&lt;/code&gt; or &lt;code&gt;March 2026&lt;/code&gt; (month name)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;15. März 2026&lt;/code&gt; (full German date)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My date extractor handles all of these and normalizes to &lt;code&gt;YYYY-MM-DD&lt;/code&gt; for filenames. The priority order: date found in document content first, then date from the filename, and finally the file modification timestamp as a last resort.&lt;/p&gt;

&lt;p&gt;Template placeholders make naming flexible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Placeholder&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{Sender}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Detected company or sender name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{Date}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Document date as YYYY-MM-DD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{YYYY}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Year (for folder paths)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ext}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Original file extension&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a rule with template &lt;code&gt;Rechnung_{Sender}_{Date}&lt;/code&gt; turns &lt;code&gt;scan_001.pdf&lt;/code&gt; into &lt;code&gt;Rechnung_Telekom_2026-03-15.pdf&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UX principle: never act without asking
&lt;/h2&gt;

&lt;p&gt;This was a hard rule from day one: &lt;strong&gt;Ablage suggests, the user decides.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a new file is detected, an in-app notification card appears with the detected type, the proposed new filename, and the target folder. Three options: &lt;strong&gt;Apply&lt;/strong&gt;, &lt;strong&gt;Customize&lt;/strong&gt; (edit before applying), or &lt;strong&gt;Skip&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No automatic moves. No silent renames. The user has to confirm — or the suggestion stays in a pending queue.&lt;/p&gt;

&lt;p&gt;Why? Because one wrong auto-move of an important file destroys all trust in the tool. Better to require a click than to lose someone's tax return.&lt;/p&gt;

&lt;p&gt;Every operation goes into a history log with full undo support. Moved a file to the wrong place? One click to reverse it.&lt;/p&gt;

&lt;h2&gt;
  
  
  First-launch onboarding
&lt;/h2&gt;

&lt;p&gt;I added a simple onboarding flow that asks you to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set a &lt;strong&gt;target folder&lt;/strong&gt; where organized files will go&lt;/li&gt;
&lt;li&gt;Add at least one &lt;strong&gt;watched folder&lt;/strong&gt; (usually Downloads)&lt;/li&gt;
&lt;li&gt;Review the default rules&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After that, drop any document into the watched folder and see Ablage in action within seconds. The onboarding made a big difference — without it, people had to dig through settings to figure out what to do first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localization
&lt;/h2&gt;

&lt;p&gt;Since this is aimed at the German market but open source for everyone, Ablage supports four languages: English, Deutsch, Українська, and Français. The language selector lives in the Settings tab and persists across sessions.&lt;/p&gt;

&lt;p&gt;Adding a new language is straightforward — just add a locale file to &lt;code&gt;src/shared/i18n/&lt;/code&gt; following the existing pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;A few lessons from building this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with fewer rules.&lt;/strong&gt; I initially had 7 default rules. For the first version, 3-4 would have been enough. Each rule needs testing with real documents, and more rules means more edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule wizard was worth the effort.&lt;/strong&gt; The inline editor is powerful but intimidating. The step-by-step wizard that asks "What keywords should I look for?" → "Where should these files go?" → "What should they be named?" got much better feedback from testers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLite was the right call.&lt;/strong&gt; I considered JSON files or electron-store for simplicity, but once you need history with undo and queryable rule sets, SQLite pays for itself. &lt;code&gt;better-sqlite3&lt;/code&gt; is synchronous and fast — no async complexity in the main process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The current version handles text-based PDFs and DOCX well. Here's what's on the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OCR for scanned documents&lt;/strong&gt; — right now, scanned PDFs are classified by filename only. Adding OCR would cover a much larger set of documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ML classification&lt;/strong&gt; — for edge cases where keyword matching fails, a lightweight model could push accuracy further.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy Shield&lt;/strong&gt; — detect and highlight personal data (IBAN, addresses, tax numbers) before sharing documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DATEV export&lt;/strong&gt; — integration with Germany's standard accounting software format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But only if there's demand. I built the simplest thing that could work, and I'm watching whether people actually use it before investing more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Ablage is free and open source under the MIT license.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/che1974/ablage" rel="noopener noreferrer"&gt;github.com/che1974/ablage&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/che1974/ablage.git
&lt;span class="nb"&gt;cd &lt;/span&gt;ablage
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run rebuild-sqlite
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you find it useful, a ⭐ on GitHub helps others discover it. Feature requests and bug reports are welcome — especially if you have document types or languages that should be supported.&lt;/p&gt;




</description>
      <category>opensource</category>
      <category>electron</category>
      <category>productivity</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
