{"result":{"depiction_url":null,"edges":{"observes":{"objects":[{"created":"2026-09-09T13:47:41Z","object_id":{"id":1349,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_user_context","title":"observe_user_context\/3","uri":"https:\/\/zotonic.com\/id\/1349"},"seq":1},{"created":"2026-09-09T13:47:41Z","object_id":{"id":1441,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_set_user_language","title":"observe_set_user_language\/2","uri":"https:\/\/zotonic.com\/id\/1441"},"seq":2},{"created":"2026-09-09T13:47:41Z","object_id":{"id":1442,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_request_context","title":"observe_request_context\/3","uri":"https:\/\/zotonic.com\/id\/1442"},"seq":3},{"created":"2026-09-09T13:47:41Z","object_id":{"id":1466,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_admin_menu","title":"observe_admin_menu\/3","uri":"https:\/\/zotonic.com\/id\/1466"},"seq":4},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2019,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_dispatch_rewrite","title":"observe_dispatch_rewrite\/3","uri":"https:\/\/zotonic.com\/id\/2019"},"seq":5},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2020,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_url_rewrite","title":"observe_url_rewrite\/3","uri":"https:\/\/zotonic.com\/id\/2020"},"seq":6},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2160,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_scomp_script_render","title":"observe_scomp_script_render\/2","uri":"https:\/\/zotonic.com\/id\/2160"},"seq":7},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2405,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_language_detect","title":"observe_language_detect\/2","uri":"https:\/\/zotonic.com\/id\/2405"},"seq":8}],"predicate":{"id":2550,"is_a":["meta","predicate"],"name":"observes","title":{"_type":"trans","tr":{"en":"Observes"}},"uri":"https:\/\/zotonic.com\/id\/observes"}},"references":{"objects":[{"created":"2020-05-30T05:47:58Z","object_id":{"id":1409,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_translation","title":{"_type":"trans","tr":{"en":"Translate interface text"}},"uri":"https:\/\/zotonic.com\/id\/1409"},"seq":1000000},{"created":"2020-05-30T05:47:58Z","object_id":{"id":1541,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_dispatch_rules","title":{"_type":"trans","tr":{"en":"Follow a request to its template"}},"uri":"https:\/\/zotonic.com\/id\/1541"},"seq":1000000},{"created":"2020-05-30T05:47:58Z","object_id":{"id":1279,"is_a":["text","documentation"],"name":"doc_glossary","title":"Glossary","uri":"https:\/\/zotonic.com\/id\/1279"},"seq":1000000}],"predicate":{"id":332,"is_a":["meta","predicate"],"name":"references","title":{"_type":"trans","tr":{"en":"References"}},"uri":"https:\/\/zotonic.com\/id\/references"}},"subject":{"objects":[{"created":"2026-09-09T13:47:41Z","object_id":{"id":2555,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_reference","title":"Reference","uri":"https:\/\/zotonic.com\/id\/2555"},"seq":1},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2561,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_content_editor","title":"Content editor","uri":"https:\/\/zotonic.com\/id\/2561"},"seq":2},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2588,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_localization_and_translation","title":"Localization and translation","uri":"https:\/\/zotonic.com\/id\/2588"},"seq":3},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2635,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_module","title":"Module","uri":"https:\/\/zotonic.com\/id\/2635"},"seq":4},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2641,"is_a":["categorization","keyword","keyword_task"],"name":"zotonic_topic_render","title":"Render","uri":"https:\/\/zotonic.com\/id\/2641"},"seq":5},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2668,"is_a":["categorization","keyword","keyword_data_type"],"name":"zotonic_topic_language_code","title":"Language code","uri":"https:\/\/zotonic.com\/id\/2668"},"seq":6},{"created":"2026-09-09T13:47:41Z","object_id":{"id":2669,"is_a":["categorization","keyword","keyword_data_type"],"name":"zotonic_topic_translated_text","title":"Translated text","uri":"https:\/\/zotonic.com\/id\/2669"},"seq":7}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":1410,"is_a":["text","documentation","reference","module"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1410"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_module_mod_translation","page_url":{"en":"https:\/\/zotonic.com\/docs\/1410\/mod_translation","x-default":"https:\/\/zotonic.com\/docs\/1410\/mod_translation"},"preview_url":null,"resource":{"version":4026,"pivot_location_lat":null,"title":"mod_translation","is_authoritative":true,"body":"<p>This module provides support for dealing with multiple languages.<\/p>\n<p>How content and static strings are translated is explained in full in <a href=\"\/id\/doc_developerguide_translation#guide-translation\">Translation<\/a>.<\/p>\n<h2>Language as part of the URL<\/h2>\n<p>By default, <a href=\"#mod-translation\">mod_translation<\/a> prefixes each URL (using <a href=\"\/id\/doc_developerguide_dispatch_rules#guide-dispatch-rewriting\">URL\nrewriting<\/a>) in your website with the code of the current\nlanguage. The idea behind this is that each language version of a <a href=\"\/id\/doc_glossary#term-resource\">resource<\/a> gets its\nown URL, and is as such indexable for Google.<\/p>\n<p>This behaviour is enabled by default, but can be switched off in the admin, by going to Structure, Translation. There is\na checkbox labelled “Show the language in the URL”.<\/p>\n<p>Alternatively you can set the config key <code>mod_translation.rewrite_url<\/code> to <code>false<\/code>.<\/p>\n<h2>Default language fallback<\/h2>\n<p>By default, the site default language is added as the first fallback after the selected languages. This makes\nuntranslated content use the site default language before other fallback languages are tried. Set\n<code>i18n.default_language_is_fallback<\/code> to <code>false<\/code> to keep the fallback language order based only on the selected request\nlanguage and the previous context language fallbacks.<\/p>\n<p>This can be changed in the admin by going to Structure, Translation. There is a checkbox labelled “Use the default\nlanguage for untranslated texts”.<\/p>\n<h2>Programmatically switching languages<\/h2>\n<p>In a template, you can use <a href=\"#mod-translation\">mod_translation<\/a>’s postback hook to switch between languages:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">{% button text=&quot;Dutch&quot; postback={set_language code=&quot;nl&quot;} delegate=`mod_translation` %}\n<\/code><\/pre>\n<p>Creates a button which switches to Dutch. And another one for english:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">{% button text=&quot;English&quot; postback={set_language code=&quot;en&quot;} delegate=`mod_translation` %}\n<\/code><\/pre>\n<h2>Supporting right-to-left languages<\/h2>\n<p>For basic use you don’t need to do anything. Zotonic base site adds a <code>lang<\/code> attribute to the html tag, and when a\nright-to-left language is selected (for instance Arabic), the browser will interpret <code>lang=&quot;ar&quot;<\/code> and automatically\nadapt the content to right-to-left.<\/p>\n<h3>Custom right-to-left content<\/h3>\n<p>If you write your own templates, you can add the <code>lang<\/code> tag in the html or body tag, for instance:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">&lt;body {% include &quot;_language_attrs.tpl&quot; id=id %} &gt;\n<\/code><\/pre>\n<p>This will generate the following, when Zotonic selected Arabic for the page with id id:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">&lt;body xml:lang=&quot;ar&quot; lang=&quot;ar&quot; dir=&quot;rtl&quot; class=&quot;rtl&quot;&gt;\n<\/code><\/pre>\n<p>When you want to add an extra class added to the rtl or ltr class you can use:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">&lt;body {% include &quot;_language_attrs.tpl&quot; id=id class=&quot;my-body-class&quot; %} &gt;\n<\/code><\/pre>\n<p>To create individual right-to-left elements, you can use the same principle:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">&lt;div {% include &quot;_language_attrs.tpl&quot; %}&gt;&lt;\/div&gt;\n<\/code><\/pre>\n<p>And when you want to force a specific language:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">&lt;div {% include &quot;_language_attrs.tpl&quot; language=`en` %} &gt;This is English content&lt;\/div&gt;\n<\/code><\/pre>\n<h2>Translating a single resource<\/h2>\n<p>Use the translation dialog in the admin or frontend editor to manage a page&#39;s languages. Select an existing source\nlanguage, a destination language, and one of these methods:<\/p>\n<ul><li><strong>Automatic translation<\/strong> sends the source texts to the configured translation service. This option is available\nwhen a module provides a translation service.<\/li><li><strong>Copy texts<\/strong> fills the destination fields with the source texts for manual translation.<\/li><li><strong>Leave texts empty<\/strong> enables the destination language so its texts can be entered manually.<\/li><\/ul>\n<p>Copying and automatic translation fill blank destination fields. Select <strong>Overwrite existing texts with new\ntranslations<\/strong> to replace filled fields as well. These actions update the edit form: review the texts and save the\npage to persist them. If automatic translation is incomplete, the editor uses the source text for missing\ntranslations and asks the user to review the result.<\/p>\n<p>The dialog also lists the page&#39;s current translations. Deleting a translation requires confirmation and disables\nthat language on the edit form; its translated texts are removed from the database when the page is saved.<\/p>\n<h3>Translating a saved resource from Erlang<\/h3>\n<p>Use <code>m_translation:add_translation\/5<\/code> to translate and save a resource directly. For example, to translate resource\n<code>Id<\/code> from English to Dutch while preserving existing Dutch texts:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">ok = m_translation:add_translation(Id, en, nl, false, Context).\n<\/code><\/pre>\n<p>The resource must be editable in <code>Context<\/code>, both languages must be editable languages for the site, and the source\nlanguage must be present on the resource. The call returns <code>ok<\/code> or <code>{error, Reason}<\/code> and waits for the translation\nservice and resource update to finish. Successful translations add the destination language and mark it for review\nin <code>translation_status<\/code>. Pass <code>true<\/code> as the fourth argument to overwrite existing destination texts.<\/p>\n<p>The resource translator processes translation records and known binary text properties, including nested blocks,\nusing the property rules described below for tree translation. Binary text is assumed to be in the selected source\nlanguage. <code>translation_translate_rsc:copy_translation\/5<\/code> copies source texts and saves the resource without calling\na translation service. <code>m_translation:remove_translation\/3<\/code> removes a language from the saved resource.<\/p>\n<p>For an in-memory property map, <code>m_translation:add_translation_map\/5<\/code> returns <code>{ok, TranslatedMap}<\/code> or an error without\nsaving a resource. <code>m_translation:remove_translation_map\/2<\/code> similarly removes languages from a map.<\/p>\n<h2>Bulk translation management<\/h2>\n<p>The admin <strong>Bulk update<\/strong> dialog offers <strong>Manage translations<\/strong> for the selected pages. It opens the language-count\nand translation dialog with automatic translation, copying, empty languages, and confirmed language removal.\nOnly the selected pages are processed, without following menu or collection descendants. Duplicate selections count\nonce, and removing a language preserves pages where it is the last language.<\/p>\n<p>Opening the dialog requires edit access to every selected page. The server stores the selection for one day and\nrestricts access to the user who created it. A sidejob processes the selection, rechecking edit access before each\npage, while the browser polls progress and displays a blocking modal. Starting a job only waits for startup, not\ncompletion. Reopening the same selection under the same user reconnects to its running job. Reload after completion\nto refresh the overview. These changes are saved immediately, independently of the bulk property update form.<\/p>\n<h2>Translating a tree<\/h2>\n<p>The <strong>Translate all pages<\/strong> button manages translations for an entire menu or collection. It is available in the\nfrontend editor and at the bottom of the admin <strong>+ Translate<\/strong> dialog for menus and pages with outgoing <code>haspart<\/code>\nconnections. Sites can use the same\nfeature for menu-based content such as timelines; the frontend editor&#39;s <code>translate-all<\/code> button acts on its <code>tree_id<\/code>.<\/p>\n<p>A tree includes its root resource, nested menu entries, and resources connected through <code>haspart<\/code>. Each resource is\ncounted and processed once, even when it occurs more than once or the tree contains a cycle. The dialog shows the\nunion of the pages&#39; languages, with the number of pages in each language. Resources without a language list count\nas using the site&#39;s default language.<\/p>\n<h3>Translation and language removal<\/h3>\n<p>The dialog offers the same translation options as the single-page editor:<\/p>\n<ul><li><strong>Automatic translation<\/strong> uses the configured translation service and marks translated content for review.<\/li><li><strong>Copy texts<\/strong> copies text from the selected source language to the destination language.<\/li><li><strong>Leave texts empty<\/strong> enables the destination language without copying or translating text.<\/li><\/ul>\n<p>Copying and automatic translation fill empty destination fields. Existing text is preserved unless the overwrite\noption is selected. Pages without the selected source language in their language list are skipped for these two\nmethods. Empty source text is allowed when the source language is listed. Before starting,\nthe dialog refreshes the page count and asks the user to confirm applying the selected options to that many pages.\nChanges are saved immediately; save or discard any pending editor changes before starting a tree operation.<\/p>\n<p>Plain binary text is also translated, assuming it is in the resource&#39;s selected source language. The shared\nresource translator recognizes <code>title<\/code>, <code>short_title<\/code>, <code>chapeau<\/code>, <code>summary<\/code>, <code>body<\/code>, <code>body_extra<\/code>, <code>date_remarks<\/code>,\n<code>prompt<\/code>, <code>explanation<\/code>, <code>matching<\/code>, <code>narrative<\/code>, <code>feedback<\/code>, <code>seo_title<\/code>, <code>seo_desc<\/code>, <code>seo_keywords<\/code>, and properties\nending in <code>_html<\/code>. This also applies inside nested blocks. These fields become translation records retaining the\nsource text alongside the destination text. Other binary properties remain unchanged; existing translation records\ncontinue to use their explicit language entries.<\/p>\n<p>Deleting a language requires a separate warning and confirmation. It removes that language from every editable\npage in the tree, except pages where it is the last language. Those pages are kept unchanged.<\/p>\n<h3>Permissions, progress, and completion<\/h3>\n<p>Opening the dialog or starting a job requires edit access to the root. Traversal requires visibility of every page.\nThe worker checks edit permission again immediately before processing each resource and skips resources that are\nno longer editable. Normal resource-update permission checks also apply when saving changes.<\/p>\n<p>Translation and removal run in a sidejob that is unique per tree and site on the current Erlang node. Startup returns\na running status as soon as the worker registers, before traversing the tree or calling a translation service. The\nrequest does not wait for completion, and the tree job has no overall translation deadline. A second job for the\nsame tree returns <code>busy<\/code> while the first is running.<\/p>\n<p>The editor shows progress in a modal that cannot be dismissed, including when an editor is opened or reloaded during\na running job. This is a UI-only lock: it does not add ACL restrictions or block unrelated server-side updates.\nProgress includes the total number of pages, pages processed, pages skipped, and pages that failed. The final\ndialog lists skipped pages with links to open their edit pages in a new tab. If an automatic\ntranslation service returns incomplete translations for a page, that page&#39;s texts are left unchanged and it is counted\nas failed. Changes already saved on other pages are retained. After completion or failure, the dialog offers a reload\nso the editor shows the saved content.<\/p>\n<h3>Model and template integration<\/h3>\n<p>The <code>m_translation_tree<\/code> model exposes these paths through templates and the model API:<\/p>\n<table class=\"table\"><thead><tr><th>Method<\/th><th>Path<\/th><th>Result<\/th><\/tr><\/thead><tbody><tr><td><code>get<\/code><\/td><td><code>\/&lt;id&gt;<\/code><\/td><td>Page total and language counts for the tree.<\/td><\/tr><tr><td><code>get<\/code><\/td><td><code>\/status\/&lt;id&gt;<\/code><\/td><td>Current or most recently cached job progress for the root.<\/td><\/tr><tr><td><code>post<\/code><\/td><td><code>\/&lt;id&gt;<\/code><\/td><td>Start a job and return its initial running status.<\/td><\/tr><\/tbody><\/table>\n<p>A translation payload contains <code>method<\/code> (<code>translate<\/code>, <code>copy<\/code>, or <code>empty<\/code>), <code>src<\/code>, <code>dst<\/code>, optional <code>overwrite<\/code>, and\n<code>confirmed: true<\/code>. A removal payload contains <code>method: remove<\/code>, <code>language<\/code>, and <code>confirmed: true<\/code>.<\/p>\n<p>The worker publishes progress on <code>model\/rsc\/event\/&lt;root&gt;\/translation_tree<\/code>. Browser clients subscribe through\n<code>bridge\/origin\/model\/rsc\/event\/&lt;root&gt;\/translation_tree<\/code> and can poll the status path to recover after reconnecting.\nRunning status is owned by the worker; completed results are cached for one hour. The initial total is zero until\ntree traversal finishes.<\/p>\n<p>Custom editors can include <code>_translation_tree_button.tpl<\/code> and <code>_translation_tree_init.tpl<\/code>, passing the root as\n<code>tree_id<\/code>. The initialization template also wires an existing button with id <code>translate-all<\/code>. The standard admin\nand frontend editors already include this initialization.<\/p>\n<h2>Accepted Events<\/h2>\n<p>This module handles the following notifier callbacks:<\/p>\n<ul><li><code>observe_admin_menu<\/code>: Add translation and language administration entries to the admin menu.<\/li><li><code>observe_dispatch_rewrite<\/code>: Removes the language from the path parts and sets it as the page language.<\/li><li><code>observe_language_detect<\/code>: Detect the preferred request language from URL, user context, and request headers.<\/li><li><code>observe_request_context<\/code>: Check if the user has a preferred language (in the user&#39;s config) using <code>z_context:get_cookie<\/code>.<\/li><li><code>observe_scomp_script_render<\/code>: Add translation runtime variables to rendered client-side script blocks.<\/li><li><code>observe_set_user_language<\/code>: Persist a user language change to the authenticated user profile when allowed.<\/li><li><code>observe_url_rewrite<\/code>: Rewrite URLs to include\/remove language prefixes according to translation settings.<\/li><li><code>observe_user_context<\/code>: Set user context language\/timezone defaults based on the user profile resource.<\/li><\/ul>\n<p>Delegate callbacks:<\/p>\n<ul><li><code>event\/2<\/code> with <code>postback<\/code> messages: <code>language_default<\/code>, <code>language_delete<\/code>, <code>language_status<\/code>, <code>set_language<\/code>, <code>toggle_url_rewrite<\/code>, <code>translation_generate<\/code>, <code>translation_reload<\/code>.<\/li><li><code>event\/2<\/code> with <code>submit<\/code> messages: <code>language_add<\/code>, <code>language_list<\/code>.<\/li><\/ul>","slug":"mod_translation","doc_module_config":[{"default":"&quot;en&quot;","description":"The default language to use for this site. Defaults to &#39;en&#39;. Note that this is set by reordering the languages in \/admin\/translation","has_default":true,"key":"language","module":"i18n","type":"string"},{"default":"&quot;en&quot;","description":"The default language stemmer to use for this site. Defaults to &#39;en&#39;. This is used for search indexing and stemming.","has_default":true,"key":"language_stemmer","module":"i18n","type":"string"},{"default":"true","description":"Add the default language as a fallback language for requests in another language. This makes untranslated texts fall back to the site default language instead of the previous request language fallback order.","has_default":true,"key":"default_language_is_fallback","module":"i18n","type":"boolean"},{"default":"true","description":"Rewrite URLs to include the language code, e.g. \/en\/ instead of \/","has_default":true,"key":"rewrite_url","module":"mod_translation","type":"boolean"},{"default":"false","description":"Force the default language if no language is set in the request. If not set then language negotation using the request&#39;s Accept-Language header is used.","has_default":true,"key":"force_default","module":"mod_translation","type":"boolean"}],"is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"32a760ddcce7d469c32934d7885011800c3c17d3daafbcec127711eef9a2ec0b","is_featured":false,"content_group_id":{"id":2551,"is_a":["meta","content_group"],"name":"content_group_imported_docs","title":"Imported documentation","uri":"https:\/\/zotonic.com\/id\/content_group_imported_docs"},"category_id":{"id":320,"is_a":["meta","category"],"name":"module","title":"Modules","uri":"https:\/\/test.zotonic.com\/id\/320"},"doc_source_path":"apps\/zotonic_mod_translation\/src\/mod_translation.erl","publication_start":"2023-06-24T08:17:44Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_mod_translation\/src\/mod_translation.erl","pivot_location_lng":null,"doc_source_kind":"module","name":"doc_module_mod_translation","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2020-05-30T05:47:21Z","uri":null,"doc_status":"current","is_dependent":false,"erlang_app":"zotonic_mod_translation","publication_end":"9999-06-01T00:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_source_commit":"c19e5c894d3bd01b57581e2af43e176d42c6877a\n","creator_id":{"id":336,"is_a":["person","robot"],"name":"gitbot","title":"Git","uri":"https:\/\/zotonic.com\/id\/336"},"modified":"2026-10-08T10:40:46Z","title_slug":"mod_translation"},"uri":"https:\/\/zotonic.com\/id\/1410","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1410"}},"status":"ok"}