<?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: John Sheehy</title>
    <description>The latest articles on DEV Community by John Sheehy (@mactifosi).</description>
    <link>https://dev.to/mactifosi</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%2F4202516%2F4cf4ec92-c522-4185-bf81-78d56b4212f4.jpg</url>
      <title>DEV Community: John Sheehy</title>
      <link>https://dev.to/mactifosi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mactifosi"/>
    <language>en</language>
    <item>
      <title>Cleaning duplicate photos inside Lightroom Classic and Capture One catalogs without breaking them</title>
      <dc:creator>John Sheehy</dc:creator>
      <pubDate>Wed, 04 Nov 2026 12:42:21 +0000</pubDate>
      <link>https://dev.to/mactifosi/cleaning-duplicate-photos-inside-lightroom-classic-and-capture-one-catalogs-without-breaking-them-4fkk</link>
      <guid>https://dev.to/mactifosi/cleaning-duplicate-photos-inside-lightroom-classic-and-capture-one-catalogs-without-breaking-them-4fkk</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm the developer of &lt;a href="https://caddora.ie/folio" rel="noopener noreferrer"&gt;Folio Refresh&lt;/a&gt;, the Mac app this post is about.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most duplicate photo finders work on folders. Point them at a drive, they hash files, they move copies to the Trash. That is fine until the photos live in a &lt;strong&gt;Lightroom Classic&lt;/strong&gt; or &lt;strong&gt;Capture One&lt;/strong&gt; catalog. Delete a file behind Lightroom's back and the catalog shows a missing-photo badge; delete the wrong copy and you lose the edits, ratings and collections attached to it.&lt;/p&gt;

&lt;p&gt;When I built Folio Refresh I wanted it to clean duplicates &lt;em&gt;inside&lt;/em&gt; those catalogs, so here is how it does that without corrupting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both catalogs are SQLite
&lt;/h2&gt;

&lt;p&gt;A Lightroom Classic catalog (&lt;code&gt;.lrcat&lt;/code&gt;) is a SQLite database. A Capture One catalog (&lt;code&gt;.cocatalog&lt;/code&gt;) is a package with a SQLite database (&lt;code&gt;.cocatalogdb&lt;/code&gt;) inside. Neither schema is documented, and both change between versions. That shapes every rule below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: read-only, always, while scanning
&lt;/h2&gt;

&lt;p&gt;Scanning only ever opens the catalog read-only.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lightroom:&lt;/strong&gt; &lt;code&gt;SQLITE_OPEN_READONLY&lt;/code&gt;. SQLite guarantees a read-only connection cannot write the database or its log, so this is safe even while Lightroom has the catalog open; it is just another reader.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture One:&lt;/strong&gt; its database keeps no write-ahead log, so while Capture One is open it is read with ordinary shared locks, and while it is closed it is opened &lt;code&gt;immutable&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before reading anything, the app checks that every table and column it uses exists. If a new Lightroom or Capture One version renames something, the catalog is left alone instead of being read wrongly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: the catalog decides what is worth keeping
&lt;/h2&gt;

&lt;p&gt;A folder can't tell you that one copy of a photo has two hours of Develop adjustments on it. The catalog can. When Folio Refresh groups duplicates, it reads what each copy carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lightroom: pick flag, star rating, Develop adjustments, collections (including the Quick Collection), and &lt;strong&gt;virtual copies&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Capture One: rating, colour tag, adjustments, albums the person made, keywords, and &lt;strong&gt;variants&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A copy someone worked on is the one suggested to keep. A photo with a virtual copy (Lightroom) or a second variant (Capture One) is never offered for removal at all, because that "duplicate" is really another edit of the same file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: never delete. Mark, and let the app delete
&lt;/h2&gt;

&lt;p&gt;Folio Refresh never deletes or moves a file inside a catalog. It changes exactly one thing, the same thing a person would change by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lightroom Classic:&lt;/strong&gt; the duplicate's master image is marked &lt;strong&gt;Rejected&lt;/strong&gt;, the same as pressing X. That is one column:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;Adobe_images&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;pick&lt;/span&gt; &lt;span class="o"&gt;=&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="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id_local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;masterImage&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The person then runs Lightroom's own &lt;em&gt;Delete Rejected Photos&lt;/em&gt;, which removes the photos from the catalog and the disk the way Lightroom expects.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capture One:&lt;/strong&gt; there is no Rejected flag, so duplicates are tagged &lt;strong&gt;Red&lt;/strong&gt;, writing the colour tag to the same metadata rows Capture One writes when you press the Red tag yourself. The person filters by Red and deletes with Capture One's own Delete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marking frees no space by itself, and that is the point: the destructive step stays in the app that owns the catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4: only write when the app is closed
&lt;/h2&gt;

&lt;p&gt;Writing to a SQLite file another process has open is how catalogs get corrupted. So before any write:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lightroom:&lt;/strong&gt; no &lt;code&gt;.lrcat-lock&lt;/code&gt; beside the catalog, and Lightroom Classic not running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture One:&lt;/strong&gt; not running, and no &lt;code&gt;writelock&lt;/code&gt; file inside the package.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If either is open, nothing is written; the app asks you to quit it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 5: back up with SQLite itself, then change everything in one transaction
&lt;/h2&gt;

&lt;p&gt;Copying the &lt;code&gt;.lrcat&lt;/code&gt; file with &lt;code&gt;FileManager&lt;/code&gt; can miss pages still in the log. Instead the backup uses SQLite's online backup API, so the copy is a complete, consistent database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;backup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sqlite3_backup_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"main"&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="k"&gt;throw&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sqlite3_backup_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backup&lt;/span&gt;&lt;span class="p"&gt;,&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="c1"&gt;// copy every page&lt;/span&gt;
&lt;span class="nf"&gt;sqlite3_backup_finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It lands in a &lt;em&gt;Folio Refresh Backups&lt;/em&gt; folder beside the catalog, named with the date and never overwriting an earlier one. If the backup fails, nothing is changed.&lt;/p&gt;

&lt;p&gt;Then all the marks go in one transaction (&lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt; … &lt;code&gt;COMMIT&lt;/code&gt;, with &lt;code&gt;ROLLBACK&lt;/code&gt; on any error), so a catalog is never left half-marked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 6: re-check right before writing, and keep an undo
&lt;/h2&gt;

&lt;p&gt;Between review and cleanup, files can change. Right before marking, each duplicate must still be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same file the person reviewed, at the same path in the catalog,&lt;/li&gt;
&lt;li&gt;not already Rejected (or Red, or in Capture One's Trash),&lt;/li&gt;
&lt;li&gt;free of virtual copies or extra variants,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and at least one copy its group keeps must still be there, unchanged. Anything that fails is skipped with a reason.&lt;/p&gt;

&lt;p&gt;Every mark is recorded with the flag it replaced, so &lt;strong&gt;Undo&lt;/strong&gt; can put the old pick flag or tag back (again only while the app is closed), and the dated backup is always there as a last resort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 7: only write to versions you have tested
&lt;/h2&gt;

&lt;p&gt;Reading works on any catalog whose columns check out. Writing is refused on a catalog version the app hasn't been tested against (&lt;code&gt;Adobe_variablesTable.Adobe_DBVersion&lt;/code&gt; for Lightroom, &lt;code&gt;ZVERSIONINFO.ZVERSION&lt;/code&gt; for Capture One). An undocumented schema is a moving target, and a refusal is cheaper than a broken catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the app, briefly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Exact copies are found by SHA-256 of the file contents, so renamed copies match.&lt;/li&gt;
&lt;li&gt;Similar shots use Apple Vision image feature prints, compared on the Mac, at three levels from near-exact to looser edits.&lt;/li&gt;
&lt;li&gt;Bursts are grouped by capture time, with a keeper suggested by sharpness and Vision's aesthetics score.&lt;/li&gt;
&lt;li&gt;Apple Photos duplicates go into a review album; deleting happens in Photos, which asks first.&lt;/li&gt;
&lt;li&gt;Nothing is uploaded and there is no account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you keep a Lightroom or Capture One catalog and have opinions about how a tool should touch it, I'd like to hear them in the comments. Folio Refresh is free to scan and to clean your first 100 photos, then $14.99 once: &lt;a href="https://caddora.ie/folio" rel="noopener noreferrer"&gt;caddora.ie/folio&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>sqlite</category>
      <category>photography</category>
    </item>
    <item>
      <title>Flutter + ML Kit: the missing dependency that reads a Japanese menu as romaji</title>
      <dc:creator>John Sheehy</dc:creator>
      <pubDate>Wed, 04 Nov 2026 12:10:26 +0000</pubDate>
      <link>https://dev.to/mactifosi/flutter-ml-kit-the-missing-dependency-that-reads-a-japanese-menu-as-romaji-2j6l</link>
      <guid>https://dev.to/mactifosi/flutter-ml-kit-the-missing-dependency-that-reads-a-japanese-menu-as-romaji-2j6l</guid>
      <description>&lt;p&gt;SofiLingo is the offline travel translator I make in Flutter, for iPhone and Android. Its camera mode reads menus, signs and tickets with Google's ML Kit text recognition, then translates them on the device.&lt;/p&gt;

&lt;p&gt;One part of that setup fails without a single error: on iOS, on Android, in &lt;code&gt;flutter analyze&lt;/code&gt;, in a Dart test or in a build log. If you use &lt;code&gt;google_mlkit_text_recognition&lt;/code&gt; with anything other than Latin script, it is worth five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  ML Kit's recognizer is per script
&lt;/h2&gt;

&lt;p&gt;ML Kit ships five on-device text recognizers: Latin, Chinese, Devanagari, Japanese and Korean. You pick one when you create the recognizer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;recognizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TextRecognizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;script:&lt;/span&gt; &lt;span class="n"&gt;TextRecognitionScript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;japanese&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrong one doesn't fail. It returns confident nonsense. Point the Latin model at a Japanese menu and you get stray romaji fragments, which look like a bad photo rather than a bug. So the app picks the recognizer from the &lt;em&gt;source&lt;/em&gt; language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;_scripts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TranslateLanguage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextRecognitionScript&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;{&lt;/span&gt;
  &lt;span class="n"&gt;TranslateLanguage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chinese&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextRecognitionScript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chinese&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;TranslateLanguage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;japanese&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextRecognitionScript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;japanese&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;TranslateLanguage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;korean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextRecognitionScript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;korean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;TranslateLanguage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hindi&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextRecognitionScript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;devanagiri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;TranslateLanguage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;marathi&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextRecognitionScript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;devanagiri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That part is in the docs. The trap is underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plugin ships Latin only
&lt;/h2&gt;

&lt;p&gt;The Flutter plugin's text recognition pod contains the Latin model and nothing else. Chinese, Japanese, Korean and Devanagari are separate native dependencies, and the plugin does not pull them in for you.&lt;/p&gt;

&lt;p&gt;Leave them out and here is what happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS:&lt;/strong&gt; nothing visible. The plugin detects which scripts were compiled in with &lt;code&gt;__has_include&lt;/code&gt;, and its switch has a bare &lt;code&gt;default:&lt;/code&gt; that returns the &lt;strong&gt;Latin&lt;/strong&gt; recognizer for any script it wasn't built with. Your code asks for Japanese, gets Latin, and the Japanese menu comes back as romaji. No exception, no log line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android:&lt;/strong&gt; the plugin declares those libraries &lt;code&gt;compileOnly&lt;/code&gt;, so they are on its compile path but absent from your APK. The first frame throws &lt;code&gt;NoClassDefFoundError&lt;/code&gt;. In a camera preview that processes frames in a loop, that throw usually lands in a per-frame &lt;code&gt;catch (_)&lt;/code&gt;, and you get a preview that never finds any text.&lt;/p&gt;

&lt;p&gt;Two platforms, two different failures, neither of them loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: declare the scripts yourself
&lt;/h2&gt;

&lt;p&gt;iOS, in &lt;code&gt;ios/Podfile&lt;/code&gt; inside the Runner target (about 1.2 to 1.6 MB each):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;pod&lt;/span&gt; &lt;span class="s1"&gt;'GoogleMLKit/TextRecognitionChinese'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 9.0.0'&lt;/span&gt;
&lt;span class="n"&gt;pod&lt;/span&gt; &lt;span class="s1"&gt;'GoogleMLKit/TextRecognitionDevanagari'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 9.0.0'&lt;/span&gt;
&lt;span class="n"&gt;pod&lt;/span&gt; &lt;span class="s1"&gt;'GoogleMLKit/TextRecognitionJapanese'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 9.0.0'&lt;/span&gt;
&lt;span class="n"&gt;pod&lt;/span&gt; &lt;span class="s1"&gt;'GoogleMLKit/TextRecognitionKorean'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 9.0.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Android, in &lt;code&gt;android/app/build.gradle.kts&lt;/code&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="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.google.mlkit:text-recognition-chinese:16.0.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.google.mlkit:text-recognition-devanagari:16.0.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.google.mlkit:text-recognition-japanese:16.0.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.google.mlkit:text-recognition-korean:16.0.1"&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;Match the versions to the ML Kit version your plugin release uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard it with a test
&lt;/h2&gt;

&lt;p&gt;Nothing in the toolchain will notice if someone tidies those lines away later, so a plain Dart test reads the two build files and fails if a script goes missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;scripts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'Chinese'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Devanagari'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Japanese'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Korean'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'the iOS pods are declared'&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="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;podfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ios/Podfile'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAsStringSync&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;final&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;podfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'GoogleMLKit/TextRecognition&lt;/span&gt;&lt;span class="si"&gt;$script&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;reason:&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$script&lt;/span&gt;&lt;span class="s"&gt; has no pod, so it silently reads as Latin'&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'the Android dependencies are declared'&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="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;gradle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'android/app/build.gradle.kts'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAsStringSync&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;final&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gradle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'text-recognition-&lt;/span&gt;&lt;span class="si"&gt;${script.toLowerCase()}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;reason:&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$script&lt;/span&gt;&lt;span class="s"&gt; is not in the APK, so it throws at the first frame'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a blunt test, but it is the only place this failure can be caught before a user points their phone at a menu.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more: list your Latin languages, don't default to them
&lt;/h2&gt;

&lt;p&gt;A related trap: if the language-to-script lookup falls back to Latin for anything it doesn't know, every new language silently claims the camera works for it. Russian through the Latin recognizer returns nothing at all, again looking like a bad photograph. SofiLingo lists its Latin-script languages explicitly, and any language in neither list has no on-device path, so it goes to cloud OCR instead (Cyrillic, Greek, Hebrew, Arabic script, Thai and every Indic script except Devanagari).&lt;/p&gt;




&lt;p&gt;SofiLingo translates menus, signs, speech and text in 58 languages, with typing that works offline, no account, and translations that stay on your phone. It's free with ads, and a one-off Holiday pass removes them for 14 days. &lt;a href="https://sofilingo.com" rel="noopener noreferrer"&gt;sofilingo.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ios</category>
      <category>android</category>
      <category>showdev</category>
    </item>
    <item>
      <title>What fills up a developer's Mac, and what is safe to clear?</title>
      <dc:creator>John Sheehy</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:52:16 +0000</pubDate>
      <link>https://dev.to/mactifosi/what-fills-up-a-developers-mac-and-what-is-safe-to-clear-2d39</link>
      <guid>https://dev.to/mactifosi/what-fills-up-a-developers-mac-and-what-is-safe-to-clear-2d39</guid>
      <description>&lt;p&gt;A developer's Mac fills up differently from anyone else's. Photos and downloads are rarely the problem; build systems are. Every tool you use keeps its own cache, every project keeps its own dependencies, and almost none of them ever clean up after themselves.&lt;/p&gt;

&lt;p&gt;The good news is that nearly all of it regenerates. The useful question is not "what can I delete" but "what does deleting it cost me" — a slower next build, a re-download, or something that does not come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Xcode and the simulators
&lt;/h2&gt;

&lt;p&gt;Usually the largest single block of space on an Apple developer's Mac.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Clearing it costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DerivedData&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Build output and indexes, one folder per project&lt;/td&gt;
&lt;td&gt;A full rebuild and re-index — &lt;a href="https://blog.refreshyourmac.com/clear-xcode-derived-data/" rel="noopener noreferrer"&gt;how to clear it&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;iOS Device Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Symbol files copied from each device you have plugged in&lt;/td&gt;
&lt;td&gt;A few minutes the next time that device connects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SwiftUI Previews&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simulators created for Xcode's canvas&lt;/td&gt;
&lt;td&gt;Previews rebuild themselves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simulator runtimes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One per iOS or watchOS version you have downloaded&lt;/td&gt;
&lt;td&gt;Several gigabytes to download again — &lt;a href="https://refreshyourmac.com/ios-simulator-runtimes/" rel="noopener noreferrer"&gt;removing old runtimes&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simulators&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each simulated device, with its apps and data&lt;/td&gt;
&lt;td&gt;The apps and data inside it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Archives&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every build you distributed&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not regenerable&lt;/strong&gt; — they hold the symbols you need to read crash reports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Archives are the one item here that is yours rather than a cache. Move old ones to another drive rather than deleting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Package managers
&lt;/h2&gt;

&lt;p&gt;Every package manager keeps a download cache, separate from the projects that use it. All of these are re-downloaded on the next install:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt; — &lt;code&gt;~/.npm/_cacache&lt;/code&gt;. &lt;code&gt;npm cache clean --force&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yarn and pnpm&lt;/strong&gt; — their own caches and stores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CocoaPods&lt;/strong&gt; — &lt;code&gt;~/Library/Caches/CocoaPods&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradle&lt;/strong&gt; — &lt;code&gt;~/.gradle/caches&lt;/code&gt;, plus downloaded Gradle versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homebrew&lt;/strong&gt; — old bottles and downloads. &lt;a href="https://blog.refreshyourmac.com/clean-homebrew-cache/" rel="noopener noreferrer"&gt;How to clean the Homebrew cache&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Inside your projects
&lt;/h2&gt;

&lt;p&gt;Each project carries dependencies and build output of its own: &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;Pods&lt;/code&gt;, Flutter's &lt;code&gt;.dart_tool&lt;/code&gt;, &lt;code&gt;.next&lt;/code&gt;, &lt;code&gt;dist&lt;/code&gt;, Rust's &lt;code&gt;target&lt;/code&gt;, Swift's &lt;code&gt;.build&lt;/code&gt;. All rebuildable, but the project will not build until you reinstall — so clear the projects you are not working on this week. &lt;a href="https://blog.refreshyourmac.com/delete-node-modules-mac/" rel="noopener noreferrer"&gt;Deleting node_modules on a Mac&lt;/a&gt; covers finding them all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android
&lt;/h2&gt;

&lt;p&gt;The emulators' disk images and the SDK's extras — the NDK, system images and the emulator itself — are only needed to build Android apps on this Mac. Each can be reinstalled from Android Studio, at the cost of a large download.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI coding tools
&lt;/h2&gt;

&lt;p&gt;Coding agents and desktop AI apps leave build trees, runtimes, update downloads and session history behind. Most of it is cache. Session history is not: it is a record of your conversations, and deleting it cannot be undone. &lt;a href="https://refreshyourmac.com/ai-tool-data-on-mac/" rel="noopener noreferrer"&gt;Where AI tools keep their data&lt;/a&gt; has the folders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that are running, not stored
&lt;/h2&gt;

&lt;p&gt;Not every developer problem is disk space. A simulator left booted, or a log watcher a tool started and forgot, keeps costing CPU and memory long after you have finished with it — which shows up as a hot Mac and a fan that will not stop, and as swap written to disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  How RefreshYourMac handles this
&lt;/h2&gt;

&lt;p&gt;RefreshYourMac was built on a developer's Mac, and these are the places it looks hardest. It measures each of the above — see &lt;a href="https://refreshyourmac.com/mac-cleaner-for-developers/" rel="noopener noreferrer"&gt;RefreshYourMac for developers&lt;/a&gt; — as its own category among its 45, sized separately and rated for what clearing it costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Safe to Remove&lt;/strong&gt; — caches that regenerate: DerivedData, iOS Device Support, SwiftUI Previews and the npm, Yarn, pnpm, CocoaPods, Gradle and Homebrew caches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; — rebuildable but not free: simulator runtimes and simulators, Android emulators and SDK extras, and your projects' build folders, each with a note on how to restore it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move to Backup&lt;/strong&gt; — Xcode archives and AI tool session history, which hold things that do not come back. There is no bulk clean here; you pick items one at a time, or move them to another drive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its &lt;strong&gt;Running Now&lt;/strong&gt; list shows booted simulators and old log watchers, with what each is costing in CPU and memory, and stops the ones you choose.&lt;/p&gt;

&lt;p&gt;Nothing is deleted without a confirmation, and a scheduled clean only ever takes Safe to Remove categories — skipping Xcode's while Xcode is open. The reasoning behind each rating is on &lt;a href="https://refreshyourmac.com/how-it-decides/" rel="noopener noreferrer"&gt;how it decides what is safe&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>xcode</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
