Do you want to make Zettlr an even better app? That's great! Whether you are simply a user, want to provide a new translation, or get into developing, you've come to the right place.
Here I describe how.
If you are simply a user who wants a good-looking, well-working writing app, just keep your eyes open for any error the app might produce (and I believe there are still some bugs to find!) and, more importantly, tell me how to make the workflow more efficient! I can only judge for my very own workflow, so to make the app better for you as well, I need to know how I could achieve this. Always remember: I cannot build a workflow as-is into the design but have to make concessions to other workflows, but I'll try to make features more accessible or working smoother as long as the trade-offs for the existing workflows and the new suggestion are not too hard.
Please report bugs by opening up issues on the GitHub repository! This way I and other contributors will be able to to quickly respond to the report and directly get to handle the problem.
Translating the App
Translating the app is very easy. All translation files are written in JSON-format, i.e. they are written in the format "placeholder": "This is the translated text".
It's a good start to just download one of the existing language files (I myself currently maintain the German, English and French translations) and just replace the text with a sentence of your own language.
The following things should be respected at all costs:
- The file must be named in <main-language>_<SUBLANGUAGE>.json, e.g.
en_US.json, etc. If there is no sub-language, just write it by repeating the main language (e.g.
- Inside some language strings there are
%s-signs. do, under no circumstances, forget them in your translated strings!!! They are used as placeholders for, e.g., the file name. So the app will, for instance, translate the string for the initialization sequence of the dictionaries like this:
Reading dictionary for %s. This may take a while…
Reading dictionary for English (UK). This may take a while…
- The same applies to %PATH% and %NAME%, etc., strings.
If you are happy with your translation, you may want to test it. Simply import it using the File-menu's "Import Translation…"-entry. Point the app to your file and it will read it in. You can then instantly switch to the newly installed language (but don't forget the obligatory restart). It will then use your provided file and you're all set!
If you notice an error or need to change something in your translated file, simply import it once again. Zettlr will automatically overwrite already existing language files and use the new ones on the next restart.
Also, please remember that at least in your language, you should add a specific key for your own language to translate it (from
en_US to the more readable
English (United States)).
Once you are happy with your translation and want it integrated officially in the app, open a pull-request to the
To start developing, simply fork the repo, work on your features, bug fixes, etc. and then open pull-requests. Please remember to only PR to the develop branch! The master-branch is only pushed to once a new release is being drafted. So if you are developing a new feature and a new version of Zettlr is released, you can simply pull the
origin master and merge it into your develop branch to be up to date again and continue writing your feature.
If you are beginning to develop a feature, it also may be wise to announce that using a new issue to just let the rest know that somebody is already doing it to maximise efficiency!
Understanding the application itself
Attention: This part hasn't been updated for a long time and is completely outdated. As the app itself is still subject to major refactions, this part won't be updated until the API is standardised!
Now it is time for some in-depth discussion of the code to help you get kick-started. I won't spend many words on it, just so that you get the first idea of the app.
If you clone the app, you will see two directories,
source. Inside the first directory there are only resources used during development that should not be integrated into the published application (mainly to keep it small in size). Among these resources are
less-files that are always compiled to
css-files prior to a release, application icons for Windows, macOS and Linux platforms and the
The pandoc templates are just being copied into the release packages to provide pandoc with default templates during export. If you want to change them, open them in the respective software (you must use Microsoft Word for editing the
docx-template because otherwise something might break) and begin changing the styles. Do not add any content to them!
You may also add your own development resources to the directory because the
.gitignore is set to ignore everything except the above mentioned directories.
source-directory there are three subdirectories. The first,
common is used for code that is used both by the main process and the renderer process. This includes some helper functions, the languages and a data-file containing some additional semi-dynamical data used by Zettlr.
main contains all classes for the main process, while the third, the
renderer-directory, contains all classes (and also
assets such as
CSS-files) that are needed for the renderer process.
Both processes are basically two gigantic trees of different objects that are constantly held in memory. The base object for the main process is an instance of the class
Zettlr. It is created during startup and not deleted until the app shuts down. Please have a look at the rather short
main.js-file, which is evaluated on startup.
In the constructor of the
Zettlr-class it loads all necessary components: First the configuration because the
ZettlrConfig-object loads the
config.json to have the configuration ready at hand (or simply creates a default object if there is no config file. If something went utterly wrong, simply delete the
config.json—it will be created anew next time the app starts).
ZettlrConfig loads both configuration variables that can be accessed using the
get() method as well as some environment variables that can be accessed using
getEnv(). The environment variables contain such sensitive things as the language. As Zettlr does not rely on the language format of the
app.getLocale() method, this is necessary. I simply don't like simple dashes in language codes. And yes, that is the only reason why this harassment is necessary.
Next, the internationalization is loaded. It is loaded from the respective file by the
i18n() method. The JSON file is simply parsed into the
global.i18n-object from where the second localization method,
trans() draws its strings to translate. The
trans() method is heavily influenced by the laravel-framework (PHP-based) using Dot-notation for language strings and tries to either find a fitting string or, if that fails, just returns the given string. This is a good indicator while translating to check which strings still need to be translated.
ZettlrIPC-class is loaded. This, as well as the renderer pendant is simply a wrapper for the
rendererIPC-classes by the electron framework, simplifying the process of sending inter process communcations. The message format is always simple: It only sends "message"s, that contain both a
command-attribute and a
content-attribute (the latter may be empty if the command does not rely on data).
After the IPC, the core of Zettlr is loaded: The directories. There are two files,
zettlr-file.js that are used for this. A
ZettlrDir-object simply assigns itself with a real path on the file system and reads it during initialization. For each directory, a new
ZettlrDirectory-object is created that itself reads its directory, while for all files that either end on
.md a new
ZettlrFile-object is created that initializes by reading the file's contents and extracting a small snippet. This structure is utterly flexible because it allows for different file types to be read into the tree. Currently, two additional types of files and directories are supported. Each
ZettlrDirectory-object may contain
ZettlrVirtualDirectory-objects representing virtual directories. They are loaded from
.ztr-virtual-directory-files and hold arbitrary selections of files. Also attachments, i.e. non-markdown-files are loaded into the
attachments-array of a
ZettlrDirectory-object and are displayed on the attachment pane. The attachment-setting in the main preferences dialog controls which files are shown.
As a last step a new
ZettlrWindow is created, e.g. the renderer process. It then initializes itself in pretty much the same way the main process does.
One single thing should be mentioned: All IPC-events are handled by a simple
switch-command in the
handleEvent() method of the main
Zettlr object. The object then decides whether it should handle the event itself or delegate the task to one of its children (i.e. the directory tree, the window or the configuration).
The renderer process is structurally very similar to the main process. Once the initial DOM is loaded into the
$(document).ready() function fires, creating a global
renderer-object. As in the main process, this holds all other objects that are created (except
CodeMirror and the
The structure of the renderer process consists of an object for each DOM object that should have some functionality attached to it:
- ZettlrBody: Controls the context menu, the dialogs and the Quicklooks, e.g. everything that affects the complete BrowserWindow (or, in other words, that could appear everywhere or needs a modal).
- ZettlrCon: Builds a context menu based on the target of the
oncontextmenuevent and displays it.
- ZettlrDialog: Displays a full-page modal and a dialog. This class is built to act tightly with the ZettlrBody-class.
- ZettlrDirectories: Handles everything that happens in the directory pane.
- ZettlrEditor: Handles the whole main editor pane and everything associated with it.
- ZettlrNotification: Is used to display small notifications in the top-right edge of the window.
- ZettlrOverlay: Basically a dialog without the dialog. It is currently only used to disable actions on the BrowserWindow until the dictionaries have been loaded (the
typo-jsobject is pretty nasty)
- ZettlrPomodoro: Controls the pomodoro timer.
- ZettlrPopup: This is used to show small popups which let you interact with different elements in the window. For instance, the rename-dialogs are all popups, as are the Pomodor-timer or the formatting-window.
- ZettlrPreview: Handles everything that happens in the preview pane. It is built to interact with the search field in the toolbar, using the ZettlrRenderer-object as a proxy (yes, the object simply forwards things between the two elements as the search field is a children of the ZettlrToolbar object)
- ZettlrQuicklook: Simply creates and displays a Quicklook window.
- ZettlrRenderer: The pendant to the Zettlr object in the renderer process.
- ZettlrRendererIPC: The pendant to the ZettlrIPC object in the renderer process
- ZettlrToolbar: Handles all buttons and actions as well as the search bar.