Open-source, fully client-side file converter. Drop a file, pick a target format, download the result — your data never leaves the browser. There is no upload, no server-side processing, and no tracking of your files.
Images · Structured data · Documents · Audio · Video — all converted locally with WebAssembly and native browser APIs.
Most online converters upload your files to a server you don't control. openConv does everything in your browser:
- 🔒 Private by architecture — files are decoded, transformed and re-encoded on your device. Your file contents are never uploaded. (Two opt-in exceptions involve metadata only — see Security & privacy.)
- ⚡ No backend — deploys as a static site; the "engine" is the user's browser.
- 🧩 Pluggable — adding a new converter is a single, self-contained module.
- 🔭 Observable — every conversion is traced and logged with OpenTelemetry, including a
traceIdsurfaced in the UI for support.
| Family | Conversions |
|---|---|
| Images | png jpeg webp gif bmp avif ico svg → png jpeg webp (quality, flattening) |
| Data | json yaml toml xml csv tsv interchange (indent, header options) |
| Documents | markdown ↔ html ↔ txt, markdown/html → pdf, pdf → txt |
| Audio | mp3 wav ogg aac flac m4a → mp3 wav ogg aac (bitrate) |
| Video | → mp4 webm, audio extraction (mp3/wav), and gif (fps, width) |
Conversion engines: native Canvas / OffscreenCanvas (images), pure-JS parsers (data), marked / turndown / jsPDF / pdf.js (documents), and ffmpeg.wasm (audio/video).
- Next.js 16 (App Router) + React 19 + TypeScript (strict,
noUncheckedIndexedAccess) - Tailwind CSS v4
- OpenTelemetry Web SDK — traces + logs with trace correlation
- Vitest + happy-dom for unit tests
- i18n — lightweight, dependency-free client-side translations (English · Türkçe · العربية) with automatic RTL
npm install
npm run dev # http://localhost:3000Other scripts:
npm run build # production build
npm run start # serve the production build
npm run typecheck # tsc --noEmit
npm run test # run the unit test suite
npm run test:coverage # tests with coverage
npm run format # prettierAll configuration is client-side and exposed via NEXT_PUBLIC_* env vars (see .env.example). Copy it to .env.local to customise.
| Variable | Default | Description |
|---|---|---|
NEXT_PUBLIC_OTEL_ENABLED |
true |
Master switch for the OpenTelemetry pipeline. |
NEXT_PUBLIC_OTEL_SERVICE_NAME |
openconv-web |
service.name reported on spans and logs. |
NEXT_PUBLIC_OTEL_ENVIRONMENT |
development |
deployment.environment.name. |
NEXT_PUBLIC_OTEL_CONSOLE |
true |
Mirror spans/logs to the browser console (trace-tagged). |
NEXT_PUBLIC_OTEL_OTLP_ENDPOINT |
(empty) | OTLP/HTTP collector base URL. Empty ⇒ no network export. |
NEXT_PUBLIC_LOG_LEVEL |
info |
debug | info | warn | error. |
When an OTLP endpoint is set, traces are sent to ${endpoint}/v1/traces and logs to ${endpoint}/v1/logs.
| Variable | Default | Description |
|---|---|---|
NEXT_PUBLIC_FFMPEG_CORE_URL |
https://unpkg.com/@ffmpeg/core@0.12.10/dist/umd |
Base URL the single-threaded ffmpeg core is fetched from. |
For offline / air-gapped deployments, host ffmpeg-core.js + ffmpeg-core.wasm yourself and point this at them (e.g. /ffmpeg). The single-threaded core needs no SharedArrayBuffer and therefore no COOP/COEP headers.
openConv ships in English, Türkçe and العربية (Arabic). The UI, error
messages, converter option labels and conversion notes are all translated; format
names (PNG, JSON, …) stay as-is since they're proper nouns.
The implementation is deliberately lightweight — no routing, no extra dependency:
- A typed dictionary per locale lives in
src/i18n/dictionaries. The English dictionary is the source of truth;trandarare typed against it, so a missing key is a compile-time error (a parity test guards it at runtime too). I18nProvider(src/i18n/provider.tsx) resolves the active locale fromlocalStorage, falling back to the browser language and then English. It mirrors the choice onto<html lang/dir>, so Arabic automatically switches the document to RTL.- Components read copy straight off the typed dictionary (
dict.hero.title). Two helpers cover the rest:interpolatefills{placeholder}tokens, andpickresolves a value by a runtime key (an errorcode, a converter optionkey, asource→targetnote) with an English fallback. Converters stay i18n-agnostic — they only expose stable keys, never localized strings.
Note: transient converter progress phases (e.g. "Encoding image") are emitted by the engine and currently surface in English; everything the user reads at rest is translated.
- Copy
src/i18n/dictionaries/en.tsto<locale>.tsand translate the values. - In
src/i18n/config.ts, add the code toLOCALES, its native name toLOCALE_NAMES, and — if right-to-left — toRTL_LOCALES. - Register the dictionary in
src/i18n/dictionaries/index.ts.
TypeScript enforces key parity, so you'll get errors until the new dictionary is complete.
openConv follows a clean / hexagonal layering. Dependencies point inward; the core knows nothing about React or OpenTelemetry.
src/
├── core/ # Domain + ports (pure, framework-agnostic)
│ ├── domain/ # FileFormat catalog, conversion model, errors
│ └── ports/ # Converter, Logger, Tracer interfaces
├── application/ # Use cases
│ ├── conversion-engine.ts # Orchestrates one conversion, fully instrumented
│ ├── converter-registry.ts # Format graph + converter lookup
│ └── base-converter.ts # Ergonomic base class for converters
├── infrastructure/ # Adapters
│ ├── observability/ # OpenTelemetry-backed Logger/Tracer + no-op fallback
│ └── converters/ # image · data · document · media plugins
├── ui/ # React providers, hooks, components
├── lib/ # Small shared utilities
└── app/ # Next.js App Router shell
- The UI detects a file's format from its name/MIME (
detectFormat). - The
ConverterRegistryexposes the reachable target formats. - The
ConversionEngineopens aconversion.executespan, resolves the rightConverter, applies option defaults, and invokes it inside a span-correlated logger/tracer context. - The converter reads the
Blob, does its work (reporting progress, honouring theAbortSignal), and returns output bytes. - The engine derives the output filename, records duration/size on the span, and returns a
ConversionResultcarrying thetraceId.
- Create
src/infrastructure/converters/<name>/. - Extend
BaseConverter: declareid,name, acapabilitiesarray (referencingFORMATS), and implementconvert(input, ctx). - Export
export const <name>Converters: Converter[] = [...]from the module'sindex.ts. - Register it in
src/infrastructure/converters/index.ts.
That's it — the registry, UI format pickers, option controls and observability wire up automatically.
Every conversion produces an OpenTelemetry trace (conversion.execute → image.decode, image.encode, data.convert, media.convert, …) and structured logs. Log records are correlated with the active span (traceId/spanId) regardless of await boundaries, because the engine hands each converter a span-scoped logger rather than relying on async context propagation. The result's traceId is shown in the UI so a user can quote it when reporting an issue.
openConv processes untrusted files entirely in your browser, so the security model is about protecting your own session from a maliciously-crafted file.
- HTML is sanitised. The
markdown/html→html/pdfpaths run output through DOMPurify before it is written to a file or rendered by jsPDF's live-DOM engine, so a converted file can never smuggle<script>oronerror=payloads into the app's origin or into the document you download. - Security headers. A Content-Security-Policy plus
X-Content-Type-Options,Referrer-Policy,X-Frame-Options,Cross-Origin-Opener-PolicyandPermissions-Policyare set innext.config.ts. The CSP pinsconnect-src/img-srcso an injected payload cannot exfiltrate, and forbids framing and plugins. These apply to a Next.js server (next start); a fully staticoutput: 'export'deployment must set them at the CDN/host. - Input size limits. Each media family has a generous per-kind cap
(
src/application/limits.ts) so an oversized file fails with a clear error instead of freezing the tab. - Two opt-in network exceptions — neither sends your file contents:
- ffmpeg.wasm core is fetched at runtime from a CDN (unpkg by default) for
audio/video conversions. Self-host it via
NEXT_PUBLIC_FFMPEG_CORE_URLfor a fully offline / no-third-party deployment. - OpenTelemetry export is off unless you set
NEXT_PUBLIC_OTEL_OTLP_ENDPOINT. When on, only conversion metadata leaves the browser (formats, sizes, the input file extension — never the full filename — timings and errors).
- ffmpeg.wasm core is fetched at runtime from a CDN (unpkg by default) for
audio/video conversions. Self-host it via
A stricter nonce-based CSP (dropping 'unsafe-inline' for scripts) is a planned
hardening step.
MIT © Mustafa Karakulak
