2019-10-17 00:42:54 +02:00
|
|
|
|
# HTML and CSS
|
|
|
|
|
|
|
|
|
|
## Zulip CSS organization
|
|
|
|
|
|
2023-05-18 21:09:16 +02:00
|
|
|
|
There are two high-level sections of CSS: the "portico" (logged-out
|
|
|
|
|
pages like `/help/`, `/login/`, etc.), and the app. The Zulip
|
|
|
|
|
application's CSS can be found in the `web/styles/` directory, while
|
|
|
|
|
the portico CSS lives under the `web/styles/portico/` subdirectory.
|
2019-10-17 00:42:54 +02:00
|
|
|
|
|
2023-05-18 21:09:16 +02:00
|
|
|
|
To generate its CSS files, Zulip uses [PostCSS](https://postcss.org/)
|
|
|
|
|
and a number of PostCSS plugins, including
|
|
|
|
|
[postcss-nesting](https://github.com/csstools/postcss-nesting#readme),
|
|
|
|
|
whose rules are derived from the [CSS Nesting](https://drafts.csswg.org/css-nesting-1/)
|
|
|
|
|
specification.
|
2019-10-17 00:42:54 +02:00
|
|
|
|
|
|
|
|
|
## Editing Zulip CSS
|
|
|
|
|
|
|
|
|
|
If you aren't experienced with doing web development and want to make
|
2023-05-18 21:08:56 +02:00
|
|
|
|
CSS changes, we recommend reading the excellent [Chrome developer tools
|
|
|
|
|
guide to the Elements panel and CSS](https://developer.chrome.com/docs/devtools/overview/#elements),
|
|
|
|
|
as well as the [section on viewing and editing CSS](https://developer.chrome.com/docs/devtools/css/)
|
2019-10-17 00:42:54 +02:00
|
|
|
|
to learn about all the great tools that you can use to modify and test
|
|
|
|
|
changes to CSS interactively in-browser (without even having the
|
|
|
|
|
reload the page!).
|
|
|
|
|
|
2023-05-19 22:23:29 +02:00
|
|
|
|
Our CSS is formatted with [Prettier](https://prettier.io/). You can
|
|
|
|
|
ask Prettier to reformat all code via our [linter
|
|
|
|
|
tool](../testing/linters.md) with `tools/lint --only=prettier --fix`.
|
|
|
|
|
You can also [integrate it with your
|
|
|
|
|
editor](https://prettier.io/docs/en/editors.html).
|
|
|
|
|
|
2023-05-18 21:09:16 +02:00
|
|
|
|
Zulip's development environment has hot code-reloading configured, so
|
2019-10-17 00:42:54 +02:00
|
|
|
|
changes made in source files will immediately take effect in open
|
|
|
|
|
browser windows, either by live-updating the CSS or reloading the
|
|
|
|
|
browser window (following backend changes).
|
|
|
|
|
|
2020-08-11 01:47:54 +02:00
|
|
|
|
## CSS style guidelines
|
2019-10-17 00:42:54 +02:00
|
|
|
|
|
|
|
|
|
### Avoid duplicated code
|
|
|
|
|
|
|
|
|
|
Without care, it's easy for a web application to end up with thousands
|
|
|
|
|
of lines of duplicated CSS code, which can make it very difficult to
|
2021-08-20 21:53:28 +02:00
|
|
|
|
understand the current styling or modify it. We would very much like
|
|
|
|
|
to avoid such a fate. So please make an effort to reuse existing
|
2019-10-17 00:42:54 +02:00
|
|
|
|
styling, clean up now-unused CSS, etc., to keep things maintainable.
|
|
|
|
|
|
2023-05-19 22:23:29 +02:00
|
|
|
|
Opt to write CSS in CSS files. Avoid using the `style=` attribute in
|
2023-05-24 19:48:05 +02:00
|
|
|
|
HTML except for styles that are set dynamically. For example, we set
|
2024-05-20 17:38:40 +02:00
|
|
|
|
the colors for specific channels (`{{stream_color}}`) on different
|
2023-05-24 19:48:05 +02:00
|
|
|
|
elements dynamically, in files like `user_stream_list_item.hbs`:
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
<span
|
|
|
|
|
class="stream-privacy-original-color-{{stream_id}} stream-privacy filter-icon"
|
|
|
|
|
style="color: {{stream_color}}">
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
But for most other cases, its preferable to define logical classes and
|
|
|
|
|
put your styles in external CSS files such as `zulip.css` or a more
|
|
|
|
|
specific CSS file, if one exists. See the contents of the `web/styles/`
|
|
|
|
|
directory.
|
2023-05-19 22:23:29 +02:00
|
|
|
|
|
2019-10-17 00:42:54 +02:00
|
|
|
|
### Be consistent with existing similar UI
|
|
|
|
|
|
|
|
|
|
Ideally, do this by reusing existing CSS declarations, so that any
|
|
|
|
|
improvements we make to the styling can improve all similar UI
|
|
|
|
|
elements.
|
|
|
|
|
|
|
|
|
|
### Use clear, unique names for classes and object IDs
|
|
|
|
|
|
|
|
|
|
This makes it much easier to read the code and use `git grep` to find
|
|
|
|
|
where a particular class is used.
|
|
|
|
|
|
2023-05-19 22:23:29 +02:00
|
|
|
|
Don't use the tag name in a selector unless you have to. In other words,
|
|
|
|
|
use `.foo` instead of `span.foo`. We shouldn't have to care if the tag
|
|
|
|
|
type changes in the future.
|
|
|
|
|
|
|
|
|
|
Additionally, multi-word class and ID values should be hyphenated,
|
|
|
|
|
also known as _kebab case_. In HTML, opt for `class="my-multiword-class"`,
|
|
|
|
|
with its corresponding CSS selector as `.my-multiword-class`.
|
|
|
|
|
|
2019-10-17 00:42:54 +02:00
|
|
|
|
## Validating CSS
|
|
|
|
|
|
|
|
|
|
When changing any part of the Zulip CSS, it's important to check that
|
|
|
|
|
the new CSS looks good at a wide range of screen widths, from very
|
|
|
|
|
wide screen (e.g. 1920px) all the way down to narrow phone screens
|
|
|
|
|
(e.g. 480px).
|
|
|
|
|
|
|
|
|
|
For complex changes, it's definitely worth testing in a few different
|
|
|
|
|
browsers to make sure things look the same.
|
|
|
|
|
|
|
|
|
|
## HTML templates
|
2017-08-22 14:54:08 +02:00
|
|
|
|
|
|
|
|
|
### Behavior
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- Templates are automatically recompiled in development when the file
|
2021-08-20 22:54:08 +02:00
|
|
|
|
is saved; a refresh of the page should be enough to display the latest
|
|
|
|
|
version. You might need to do a hard refresh, as some browsers cache
|
|
|
|
|
webpages.
|
2017-08-22 14:54:08 +02:00
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- Variables can be used in templates. The variables available to the
|
2021-08-20 22:54:08 +02:00
|
|
|
|
template are called the **context**. Passing the context to the HTML
|
|
|
|
|
template sets the values of those variables to the value they were
|
|
|
|
|
given in the context. The sections below contain specifics on how the
|
|
|
|
|
context is defined and where it can be found.
|
2017-08-22 14:54:08 +02:00
|
|
|
|
|
2019-10-17 00:42:54 +02:00
|
|
|
|
### Backend templates
|
2017-08-22 14:54:08 +02:00
|
|
|
|
|
|
|
|
|
For text generated in the backend, including logged-out ("portico")
|
2021-05-14 00:16:30 +02:00
|
|
|
|
pages and the web app's base content, we use the [Jinja2][] template
|
2017-08-22 14:54:08 +02:00
|
|
|
|
engine (files in `templates/zerver`).
|
|
|
|
|
|
|
|
|
|
The syntax for using conditionals and other common structures can be
|
|
|
|
|
found [here][jconditionals].
|
|
|
|
|
|
2023-10-06 20:05:10 +02:00
|
|
|
|
The context for Jinja2 templates is assembled from a couple places:
|
2017-08-22 14:54:08 +02:00
|
|
|
|
|
2021-08-20 21:53:28 +02:00
|
|
|
|
- `zulip_default_context` in `zerver/context_processors.py`. This is
|
2021-08-20 22:54:08 +02:00
|
|
|
|
the default context available to all Jinja2 templates.
|
2017-08-22 14:54:08 +02:00
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- As an argument in the `render` call in the relevant function that
|
2021-08-20 22:54:08 +02:00
|
|
|
|
renders the template. For example, if you want to find the context
|
|
|
|
|
passed to `index.html`, you can do:
|
2017-08-22 14:54:08 +02:00
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
|
```console
|
2018-04-22 07:02:19 +02:00
|
|
|
|
$ git grep zerver/app/index.html '*.py'
|
|
|
|
|
zerver/views/home.py: response = render(request, 'zerver/app/index.html',
|
2017-08-22 14:54:08 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The next line in the code being the context definition.
|
|
|
|
|
|
2019-10-17 00:42:54 +02:00
|
|
|
|
### Frontend templates
|
2017-08-22 14:54:08 +02:00
|
|
|
|
|
|
|
|
|
For text generated in the frontend, live-rendering HTML from
|
|
|
|
|
JavaScript for things like the main message feed, we use the
|
2023-02-22 23:03:47 +01:00
|
|
|
|
[Handlebars][] template engine (files in `web/templates/`) and
|
2017-08-22 14:54:08 +02:00
|
|
|
|
sometimes work directly from JavaScript code (though as a policy
|
|
|
|
|
matter, we try to avoid generating HTML directly in JavaScript
|
|
|
|
|
wherever possible).
|
|
|
|
|
|
|
|
|
|
The syntax for using conditionals and other common structures can be
|
|
|
|
|
found [here][hconditionals].
|
|
|
|
|
|
|
|
|
|
There's no equivalent of `zulip_default_context` for the Handlebars
|
|
|
|
|
templates.
|
|
|
|
|
|
2018-04-28 19:44:19 +02:00
|
|
|
|
### Toolchain
|
|
|
|
|
|
2019-06-25 11:39:03 +02:00
|
|
|
|
Handlebars is in our `package.json` and thus ends up in `node_modules`; We use
|
|
|
|
|
handlebars-loader to load and compile templates during the webpack bundling
|
|
|
|
|
stage. In the development environment, webpack will trigger a browser reload
|
|
|
|
|
whenever a template is changed.
|
2018-04-28 19:44:19 +02:00
|
|
|
|
|
2017-08-22 14:54:08 +02:00
|
|
|
|
### Translation
|
|
|
|
|
|
|
|
|
|
All user-facing strings (excluding pages only visible to sysadmins or
|
2021-11-30 22:05:10 +01:00
|
|
|
|
developers) should be tagged for [translation][trans].
|
2017-08-22 14:54:08 +02:00
|
|
|
|
|
2021-05-10 06:54:51 +02:00
|
|
|
|
### Tooltips
|
|
|
|
|
|
|
|
|
|
Zulip uses [TippyJS](https://atomiks.github.io/tippyjs/) for its tooltips.
|
|
|
|
|
|
2019-10-23 18:23:25 +02:00
|
|
|
|
## Static asset pipeline
|
|
|
|
|
|
|
|
|
|
This section documents additional information that may be useful when
|
|
|
|
|
developing new features for Zulip that require front-end changes,
|
|
|
|
|
especially those that involve adding new files. For a more general
|
|
|
|
|
overview, see the [new feature tutorial](../tutorials/new-feature-tutorial.md).
|
|
|
|
|
|
2022-02-24 00:17:21 +01:00
|
|
|
|
Our [dependencies documentation](dependencies.md) has useful
|
2019-10-23 18:23:25 +02:00
|
|
|
|
relevant background as well.
|
|
|
|
|
|
|
|
|
|
### Primary build process
|
|
|
|
|
|
2023-02-22 23:03:47 +01:00
|
|
|
|
Zulip's frontend is primarily JavaScript in the `web/src` directory;
|
2021-08-20 21:53:28 +02:00
|
|
|
|
we are working on migrating these to TypeScript modules. Stylesheets
|
2021-04-15 22:57:23 +02:00
|
|
|
|
are written in CSS extended by various PostCSS plugins; they are
|
|
|
|
|
converted from plain CSS, and we have yet to take full advantage of
|
2021-08-20 21:53:28 +02:00
|
|
|
|
the features PostCSS offers. We use Webpack to transpile and build JS
|
2019-10-25 09:45:13 +02:00
|
|
|
|
and CSS bundles that the browser can understand, one for each entry
|
2023-02-22 23:03:47 +01:00
|
|
|
|
points specified in `web/webpack.*assets.json`; source maps are
|
2019-10-25 09:45:13 +02:00
|
|
|
|
generated in the process for better debugging experience.
|
2019-10-23 18:23:25 +02:00
|
|
|
|
|
|
|
|
|
In development mode, bundles are built and served on the fly using
|
|
|
|
|
webpack-dev-server with live reloading. In production mode (and when creating a
|
|
|
|
|
release tarball using `tools/build-release-tarball`), the
|
|
|
|
|
`tools/update-prod-static` tool (called by both `tools/build-release-tarball`
|
|
|
|
|
and `tools/upgrade-zulip-from-git`) is responsible for orchestrating the
|
|
|
|
|
webpack build, JS minification and a host of other steps for getting the assets
|
|
|
|
|
ready for deployment.
|
|
|
|
|
|
|
|
|
|
You can trace which source files are included in which HTML templates
|
2021-03-17 03:11:19 +01:00
|
|
|
|
by comparing the `entrypoint` variables in the HTML templates under
|
2023-02-22 23:03:47 +01:00
|
|
|
|
`templates/` with the bundles declared in `web/webpack.*assets.json`.
|
2019-10-23 18:23:25 +02:00
|
|
|
|
|
|
|
|
|
### Adding static files
|
|
|
|
|
|
2020-09-15 22:23:01 +02:00
|
|
|
|
To add a static file to the app (JavaScript, TypeScript, CSS, images, etc),
|
2019-10-23 18:23:25 +02:00
|
|
|
|
first add it to the appropriate place under `static/`.
|
|
|
|
|
|
|
|
|
|
- Third-party packages from the NPM repository should be added to
|
2023-03-20 19:52:59 +01:00
|
|
|
|
`package.json` for management by pnpm, this allows them to be upgraded easily
|
|
|
|
|
and not bloat our codebase. Run `./tools/provision` for pnpm to install the
|
2019-10-23 18:23:25 +02:00
|
|
|
|
new packages and update its lock file. You should also update
|
2023-03-06 05:56:28 +01:00
|
|
|
|
`PROVISION_VERSION` in `version.py` in the same commit.
|
2019-10-23 18:23:25 +02:00
|
|
|
|
- Third-party files that we have patched should all go in
|
2023-02-22 23:03:47 +01:00
|
|
|
|
`web/third/`. Tag the commit with "[third]" when adding or
|
2021-08-20 21:53:28 +02:00
|
|
|
|
modifying a third-party package. Our goal is to the extent possible
|
2019-10-23 18:23:25 +02:00
|
|
|
|
to eliminate patched third-party code from the project.
|
2023-02-22 23:03:47 +01:00
|
|
|
|
- Our own JavaScript and TypeScript files live under `web/src`. Ideally,
|
2019-10-23 18:23:25 +02:00
|
|
|
|
new modules should be written in TypeScript (details on this policy below).
|
2023-02-22 23:03:47 +01:00
|
|
|
|
- CSS files live under `web/styles`.
|
2019-10-23 18:23:25 +02:00
|
|
|
|
- Portico JavaScript ("portico" means for logged-out pages) lives under
|
2023-02-22 23:03:47 +01:00
|
|
|
|
`web/src/portico`.
|
|
|
|
|
- Custom SVG graphics living under `web/images/icons` are compiled into
|
2019-10-23 18:23:25 +02:00
|
|
|
|
custom icon webfonts by webfont-loader according to the
|
2023-02-22 23:03:47 +01:00
|
|
|
|
`web/images/icons/template.hbs` template.
|
2019-10-23 18:23:25 +02:00
|
|
|
|
|
|
|
|
|
For your asset to be included in a development/production bundle, it
|
2021-04-15 22:57:23 +02:00
|
|
|
|
needs to be accessible from one of the entry points defined either in
|
2023-02-22 23:03:47 +01:00
|
|
|
|
`web/webpack.assets.json` or `web/webpack.dev-assets.json`.
|
2019-10-23 18:23:25 +02:00
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- If you plan to only use the file within the app proper, and not on the login
|
2019-10-23 18:23:25 +02:00
|
|
|
|
page or other standalone pages, put it in the `app` bundle by importing it
|
2023-03-11 08:13:37 +01:00
|
|
|
|
in `web/src/bundles/app.ts`.
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- If it needs to be available both in the app and all
|
2019-10-23 18:23:25 +02:00
|
|
|
|
logged-out/portico pages, import it to
|
2023-03-11 08:13:37 +01:00
|
|
|
|
`web/src/bundles/common.ts` which itself is imported to the
|
2019-10-23 18:23:25 +02:00
|
|
|
|
`app` and `common` bundles.
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- If it's just used on a single standalone page which is only used in
|
2021-04-15 22:57:23 +02:00
|
|
|
|
a development environment (e.g. `/devlogin`) create a new entry
|
2023-02-22 23:03:47 +01:00
|
|
|
|
point in `web/webpack.dev-assets.json` or it's used in both
|
2021-04-15 22:57:23 +02:00
|
|
|
|
production and development (e.g. `/stats`) create a new entry point
|
2023-02-22 23:03:47 +01:00
|
|
|
|
in `web/webpack.assets.json`. Use the `bundle` macro (defined in
|
2021-04-15 22:57:23 +02:00
|
|
|
|
`templates/zerver/base.html`) in the relevant Jinja2 template to
|
|
|
|
|
inject the compiled JS and CSS.
|
2019-10-23 18:23:25 +02:00
|
|
|
|
|
|
|
|
|
If you want to test minified files in development, look for the
|
2020-08-19 21:55:28 +02:00
|
|
|
|
`DEBUG =` line in `zproject/default_settings.py` and set it to `False`.
|
2019-10-23 18:23:25 +02:00
|
|
|
|
|
|
|
|
|
### How it works in production
|
|
|
|
|
|
|
|
|
|
A few useful notes are:
|
2021-08-20 22:54:08 +02:00
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- Zulip installs static assets in production in
|
2021-08-20 22:54:08 +02:00
|
|
|
|
`/home/zulip/prod-static`. When a new version is deployed, before the
|
|
|
|
|
server is restarted, files are copied into that directory.
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- We use the VFL (versioned file layout) strategy, where each file in
|
2019-10-23 18:23:25 +02:00
|
|
|
|
the codebase (e.g. `favicon.ico`) gets a new name
|
2021-08-20 21:53:28 +02:00
|
|
|
|
(e.g. `favicon.c55d45ae8c58.ico`) that contains a hash in it. Each
|
2019-10-23 18:23:25 +02:00
|
|
|
|
deployment, has a manifest file
|
|
|
|
|
(e.g. `/home/zulip/deployments/current/staticfiles.json`) that maps
|
2021-08-20 21:53:28 +02:00
|
|
|
|
codebase filenames to serving filenames for that deployment. The
|
2019-10-23 18:23:25 +02:00
|
|
|
|
benefit of this VFL approach is that all the static files for past
|
|
|
|
|
deployments can coexist, which in turn eliminates most classes of
|
|
|
|
|
race condition bugs where browser windows opened just before a
|
2021-08-20 21:53:28 +02:00
|
|
|
|
deployment can't find their static assets. It also is necessary for
|
2019-10-23 18:23:25 +02:00
|
|
|
|
any incremental rollout strategy where different clients get
|
|
|
|
|
different versions of the site.
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- Some paths for files (e.g. emoji) are stored in the
|
2019-10-23 18:23:25 +02:00
|
|
|
|
`rendered_content` of past messages, and thus cannot be removed
|
|
|
|
|
without breaking the rendering of old messages (or doing a
|
|
|
|
|
mass-rerender of old messages).
|
|
|
|
|
|
2021-03-29 20:33:52 +02:00
|
|
|
|
### ES6/TypeScript modules
|
|
|
|
|
|
|
|
|
|
JavaScript modules in the frontend are [ES6
|
|
|
|
|
modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
|
|
|
|
|
that are [transpiled by
|
|
|
|
|
webpack](https://webpack.js.org/api/module-methods/#es6-recommended).
|
|
|
|
|
Any variable, function, etc. can be made public by adding the
|
|
|
|
|
[`export`
|
|
|
|
|
keyword](https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export),
|
|
|
|
|
and consumed from another module using the [`import`
|
|
|
|
|
statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import).
|
2019-10-23 18:23:25 +02:00
|
|
|
|
|
|
|
|
|
New modules should ideally be written in TypeScript (though in cases
|
|
|
|
|
where one is moving code from an existing JavaScript module, the new
|
|
|
|
|
commit should just move the code, not translate it to TypeScript).
|
|
|
|
|
TypeScript provides more accurate information to development tools,
|
2021-03-29 20:33:52 +02:00
|
|
|
|
allowing for better refactoring, auto-completion and static analysis.
|
2021-08-20 21:53:28 +02:00
|
|
|
|
TypeScript also uses the ES6 module system. See our documentation on
|
2021-03-29 20:33:52 +02:00
|
|
|
|
[TypeScript static types](../testing/typescript).
|
|
|
|
|
|
|
|
|
|
Webpack does not ordinarily allow modules to be accessed directly from
|
|
|
|
|
the browser console, but for debugging convenience, we have a custom
|
2023-02-22 23:03:47 +01:00
|
|
|
|
webpack plugin (`web/debug-require-webpack-plugin.ts`) that exposes
|
2021-03-29 20:33:52 +02:00
|
|
|
|
a version of the `require()` function to the development environment
|
2021-08-20 21:53:28 +02:00
|
|
|
|
browser console for this purpose. For example, you can access our
|
2021-09-08 00:23:24 +02:00
|
|
|
|
`people` module by evaluating
|
2023-02-24 02:20:53 +01:00
|
|
|
|
`people = require("./src/people")`, or the third-party `lodash`
|
2021-08-20 21:53:28 +02:00
|
|
|
|
module with `_ = require("lodash")`. This mechanism is **not** a
|
2021-09-08 00:23:24 +02:00
|
|
|
|
stable API and should not be used for any purpose other than
|
|
|
|
|
interactive debugging.
|
2021-03-29 20:33:52 +02:00
|
|
|
|
|
|
|
|
|
We have one module, `zulip_test`, that’s exposed as a global variable
|
|
|
|
|
using `expose-loader` for direct use in Puppeteer tests and in the
|
2021-08-20 21:53:28 +02:00
|
|
|
|
production browser console. If you need to access a variable or
|
|
|
|
|
function in those scenarios, add it to `zulip_test`. This is also
|
2021-03-29 20:33:52 +02:00
|
|
|
|
**not** a stable API.
|
2019-10-23 18:23:25 +02:00
|
|
|
|
|
2021-08-20 22:54:08 +02:00
|
|
|
|
[jinja2]: http://jinja.pocoo.org/
|
|
|
|
|
[handlebars]: https://handlebarsjs.com/
|
2021-11-30 22:05:10 +01:00
|
|
|
|
[trans]: https://jinja.palletsprojects.com/en/3.0.x/extensions/#i18n-extension
|
2017-08-22 14:54:08 +02:00
|
|
|
|
[jconditionals]: http://jinja.pocoo.org/docs/2.9/templates/#list-of-control-structures
|
2023-05-18 21:08:56 +02:00
|
|
|
|
[hconditionals]: https://handlebarsjs.com/guide/block-helpers.html#block-helpers
|
2019-09-30 19:37:56 +02:00
|
|
|
|
[translation]: ../translating/translating.md
|