Files
vnidrop/localization
cdricms cc194f6a7b feat(apple): add direct-download macOS channel (notarized DMG + Sparkle + Homebrew)
Add a second macOS shipping channel alongside the App Store build:

- New VniDropDirect target (Release-Direct config) sharing VniDrop's sources via
  an AppBase target template; links Sparkle behind the DIRECT_DISTRIBUTION flag so
  the App Store binary never bundles a self-updater. arm64-only (core is arm64).
- Sparkle updater (SparkleUpdater.swift) + "Check for Updates" menu, compiled only
  under DIRECT_DISTRIBUTION; Info.plist SUFeedURL points at the GitHub Release
  /latest/download/appcast.xml, non-sandboxed entitlements for Developer ID.
- build-dmg.sh (archive → Developer ID export → DMG → sign → notarize → staple),
  generate-appcast.sh, and ExportOptions-DeveloperID.plist.
- apple-release.yml: on tag v*.*.*, build/notarize the DMG, publish the GitHub
  Release with appcast, and push the Homebrew cask to sudosylabs/homebrew-vnidrop.
  apple.yml gains a PR compile-check of the direct target.
- CFBundleVersion is stamped at build time as a UTC YYMMDD.HHMM timestamp for both
  channels, replacing the hand-maintained build number.
- Docs (RELEASE-MACOS.md, README), cask template + tap README, localized
  updates_check string, Makefile targets, gitignore for dist/ artifacts.
2026-07-27 14:31:36 +02:00
..

Localization

Single source of truth for every user-facing string, in strings.json. A Bun CLI generates the platform-native files from it:

Target Output Notes
apple apple/VniDrop/Resources/Localizable.xcstrings one catalog, all languages nested
kmp shared/src/commonMain/composeResources/values[-lang]/strings.xml one file per language

Workflow

# From the repository root:
make check-localization      # structural checks (run before committing)
make localization            # regenerate .xcstrings + strings.xml from strings.json
make localization-migrate    # one-time: rebuild strings.json from platform files

Never edit the generated .xcstrings / strings.xml by hand — edit strings.json and regenerate. Regenerated output is deterministic (sorted keys), so diffs stay small.

strings.json format

{
  "sourceLanguage": "en",
  "supportedLanguages": ["en", "fr"],
  "strings": {
    "send_title": {
      "context": "Send tab — screen title.",
      "translations": { "en": "Send", "fr": "Envoyer" }
    },

    "send_selected_files_count": {
      "context": "Send flow — number of files chosen before creating a transfer.",
      "targets": ["kmp", "apple"],
      "args": [{ "name": "count", "type": "int" }],
      "plural": {
        "en": { "one": "{count} file selected", "other": "{count} files selected" },
        "fr": { "one": "{count} fichier sélectionné", "other": "{count} fichiers sélectionnés" }
      }
    }
  }
}

Fields

  • context (required) — where the string appears and its purpose. Emitted as the .xcstrings comment and an XML comment; also the note translators see.
  • targets (optional)["kmp", "apple"]. Omit to mean all targets.
  • args (optional) — ordered list of { name, type }, typestring | int | double. Referenced in text as {name}.
  • translations — flat text per language. Mutually exclusive with plural.
  • plural — per language, per CLDR category (zero, one, two, few, many, other). other is always required.

Placeholders

Write named tokens {count}, {name} in text. The generator converts them to the right positional token per platform, using the declared type:

type Apple Android/KMP
string %N$@ %N$s
int %N$d %N$d
double %N$f %N$f

A literal % in text is emitted as %% whenever the string has args.

Adding a language

Add its code to supportedLanguages, fill in translations / plural for each key, then generate. KMP gets a new values-<lang>/strings.xml; Apple gets the language inside the single catalog. validate warns about any key still missing that language.

Migration notes (from the initial import)

  • Apple keys that were literal English strings ("%@ · %@") were imported verbatim — rename them to semantic keys and update the Swift call sites.
  • Arg names default to arg1, arg2… (a lone int arg becomes count). Rename for clarity; keep the {token} in text in sync.
  • Folding transfer_file_count_one / _other into the plural key transfer_file_count requires switching the KMP call site from Res.string.transfer_file_count_one to the Compose plural API (pluralStringResource(Res.plurals.transfer_file_count, count, count)), and the Apple side to automatic plural inflection.