{"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"}}},"export_revision":4164,"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":4164,"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>Language recognition<\/h2>\n<p>When a resource is saved without a language, Zotonic can infer one from its text.\nIt extracts plain text from <code>title<\/code>, <code>chapeau<\/code>, <code>subtitle<\/code>, <code>summary<\/code>, and <code>body<\/code>,\nthen compares its n-gram statistics with trained language profiles. Short input\n(at most 10 bytes after stripping HTML and trimming) or an unsuccessful detection\nfalls back to the current context language. Detection is a heuristic; review the\nselected language when importing short or multilingual content.<\/p>\n<p><code>translation_detect:detect(Text, Context)<\/code> restricts matches to the site&#39;s editable\nlanguages; <code>translation_detect:detect(Text)<\/code> considers all installed profiles.\nBoth return <code>{ok, LanguageCode}<\/code> or <code>{error, nomatch}<\/code>. Recognition identifies the\nlanguage of existing text; it does not call an automatic translation service.<\/p>\n<p>The bundled profiles in <code>priv\/data\/profiles<\/code> cover:<\/p>\n<table class=\"table\"><thead><tr><th>Code<\/th><th>Language<\/th><\/tr><\/thead><tbody><tr><td><code>af<\/code><\/td><td>Afrikaans<\/td><\/tr><tr><td><code>ar<\/code><\/td><td>Arabic<\/td><\/tr><tr><td><code>be<\/code><\/td><td>Belarusian<\/td><\/tr><tr><td><code>bg<\/code><\/td><td>Bulgarian<\/td><\/tr><tr><td><code>bs<\/code><\/td><td>Bosnian<\/td><\/tr><tr><td><code>cs<\/code><\/td><td>Czech<\/td><\/tr><tr><td><code>da<\/code><\/td><td>Danish<\/td><\/tr><tr><td><code>de<\/code><\/td><td>German<\/td><\/tr><tr><td><code>el<\/code><\/td><td>Greek<\/td><\/tr><tr><td><code>en<\/code><\/td><td>English<\/td><\/tr><tr><td><code>es<\/code><\/td><td>Spanish<\/td><\/tr><tr><td><code>et<\/code><\/td><td>Estonian<\/td><\/tr><tr><td><code>fa<\/code><\/td><td>Persian<\/td><\/tr><tr><td><code>fi<\/code><\/td><td>Finnish<\/td><\/tr><tr><td><code>fr<\/code><\/td><td>French<\/td><\/tr><tr><td><code>fy<\/code><\/td><td>Frisian<\/td><\/tr><tr><td><code>ga<\/code><\/td><td>Irish<\/td><\/tr><tr><td><code>he<\/code><\/td><td>Hebrew<\/td><\/tr><tr><td><code>hi<\/code><\/td><td>Hindi<\/td><\/tr><tr><td><code>hr<\/code><\/td><td>Croatian<\/td><\/tr><tr><td><code>hu<\/code><\/td><td>Hungarian<\/td><\/tr><tr><td><code>id<\/code><\/td><td>Indonesian<\/td><\/tr><tr><td><code>is<\/code><\/td><td>Icelandic<\/td><\/tr><tr><td><code>it<\/code><\/td><td>Italian<\/td><\/tr><tr><td><code>ja<\/code><\/td><td>Japanese<\/td><\/tr><tr><td><code>ko<\/code><\/td><td>Korean<\/td><\/tr><tr><td><code>lt<\/code><\/td><td>Lithuanian<\/td><\/tr><tr><td><code>ms<\/code><\/td><td>Malay<\/td><\/tr><tr><td><code>nl<\/code><\/td><td>Dutch<\/td><\/tr><tr><td><code>no<\/code><\/td><td>Norwegian<\/td><\/tr><tr><td><code>pl<\/code><\/td><td>Polish<\/td><\/tr><tr><td><code>pt<\/code><\/td><td>Portuguese<\/td><\/tr><tr><td><code>ro<\/code><\/td><td>Romanian<\/td><\/tr><tr><td><code>ru<\/code><\/td><td>Russian<\/td><\/tr><tr><td><code>si<\/code><\/td><td>Sinhalese<\/td><\/tr><tr><td><code>sk<\/code><\/td><td>Slovak<\/td><\/tr><tr><td><code>sl<\/code><\/td><td>Slovenian<\/td><\/tr><tr><td><code>sv<\/code><\/td><td>Swedish<\/td><\/tr><tr><td><code>sr<\/code><\/td><td>Serbian<\/td><\/tr><tr><td><code>sq<\/code><\/td><td>Albanian<\/td><\/tr><tr><td><code>ta<\/code><\/td><td>Tamil<\/td><\/tr><tr><td><code>th<\/code><\/td><td>Thai<\/td><\/tr><tr><td><code>tl<\/code><\/td><td>Tagalog<\/td><\/tr><tr><td><code>tr<\/code><\/td><td>Turkish<\/td><\/tr><tr><td><code>uk<\/code><\/td><td>Ukrainian<\/td><\/tr><tr><td><code>vi<\/code><\/td><td>Vietnamese<\/td><\/tr><tr><td><code>xh<\/code><\/td><td>Xhosa<\/td><\/tr><tr><td><code>zh-hant<\/code><\/td><td>Traditional Chinese (Cantonese training text)<\/td><\/tr><tr><td><code>zh<\/code><\/td><td>Chinese<\/td><\/tr><\/tbody><\/table>\n<p>Training texts come from Wikipedia, typically articles about a country or language.\nCountry articles also include names characteristic of the language.<\/p>\n<h3>Adding a recognition profile<\/h3>\n<ol><li>Add a UTF-8 training file named <code>&lt;language-code&gt;.txt<\/code> to <code>priv\/data\/texts<\/code>.\nThe code must be recognized by <code>z_language<\/code>.<\/li><li>In the Erlang shell, run <code>translation_detect:generate_profile_data().<\/code> This\ngenerates profiles from the training files and loads the profile data.<\/li><li>Check the training text and generated <code>priv\/data\/profiles\/&lt;language-code&gt;.dat<\/code>\ninto version control. Review other generated files before committing them.<\/li><li>To load newly installed profiles without regenerating them, run\n<code>translation_detect:load_profile_data().<\/code> Use <code>translation_detect:profiles()<\/code>\nto list the installed profile codes.<\/li><\/ol>\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>Editing texts in the admin<\/h2>\n<p>The resource translation overview opens a single-field dialog when an editable\ntext is clicked. Body properties and properties ending in <code>_html<\/code> use <code>z_editor<\/code>;\nshort plain texts use an input and other plain texts use a textarea. Sites can\noverride the blocks in <code>_translation_edit_field.tpl<\/code>. The rich-text dialog inherits\nthe page&#39;s editor profile (advanced in the admin) and works with any enabled editor module.<\/p>\n<p>Saving changes only the selected property and language, preserving other languages\nand block structure. Named block fields are supported; structured survey answer\nlists, unnamed blocks and unsafe properties remain read-only. Missing translations\nopen empty, without falling back to a different language.\nThe page slug is editable only when the resource has Customize page slug enabled\n(<code>custom_slug<\/code>). This condition is checked when opening and saving the dialog.<\/p>\n<p>The translation overview, admin and frontend-admin edit forms warn when a newer\nresource version is observed. A successful translation dialog save acknowledges\nits new version in the overview only if the overview was current when the dialog\nopened. Saves never clear warnings about intervening external updates.\nResource update notifications include <code>version<\/code>, including updates through\n<code>m_rsc:touch\/2<\/code>. Media notifications also trigger a version check. Reconnects,\nwindow focus and periodic checks recover missed notifications. Notifications never\nadvance the version of an unsaved form, and stale saves are rejected under a database\nrow lock. Reloading asks for confirmation when the form has unsaved edits.<\/p>\n<p>Media replacements initiated in the edit page send a private version acknowledgement\nto that browser. The form advances only from the exact pre-update version and keeps\nits unsaved edits; other tabs and already-stale forms still show the warning.<\/p>\n<p>Video conversion marks resource updates with the update option\n<code>{update_reason, media_processing}<\/code>. Resource update notifications include\n<code>previous_version<\/code> and <code>reason<\/code>. The <code>media_processing<\/code> reason is emitted only when\nthe resource properties are unchanged except for version, modification date and\nmodifier. Otherwise the reason is <code>update<\/code>, including when upload observers change\ntext or category. Editors and translation overviews acknowledge only the next\nconsecutive processing version, preserving unsaved edits and existing conflicts.\nMissed notifications conservatively require reloading; polling alone cannot prove\nthat a skipped version contained no text changes. Failed conversion and preview-only\nmedium updates do not themselves increment the resource version.<\/p>\n<p>The single-field edit dialog offers <strong>Translate from<\/strong> when a translation service\nand another nonempty source language are available. It translates the saved source\nproperty (including named-block fields) using the configured services, with HTML\nmode for rich text and text mode otherwise. The result replaces only the dialog&#39;s\ndraft, after confirmation; Save still performs the normal version-checked update.\nSource languages use exact translations, not fallback. Failed requests and results\narriving after further edits or closing the dialog do not overwrite the draft.\nResource permissions, field eligibility and source\/target languages are rechecked\non the server. Automatic translation may send the source text to a remote service.<\/p>\n<p>Custom admin form templates must include <code>_admin_edit_version.tpl<\/code> inside the form\n(pass <code>version_form<\/code> for a form id other than <code>rscform<\/code>). The <code>z_edit_version<\/code>\nhidden input is required for version-checked saves.<\/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>: Recognize the language of supplied text using n-gram profiles, optionally restricted to editable languages.<\/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":"f7e0e3ffb9957a61426289fc33734f696c310c870673ec0be0d1c742c37bf1ea","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":"f3c6a9a2dde17cb5b121dd3e3018d9ef4b1b3943\n","creator_id":{"id":336,"is_a":["person","robot"],"name":"gitbot","title":"Git","uri":"https:\/\/zotonic.com\/id\/336"},"modified":"2026-11-09T21:39:23Z","title_slug":"mod_translation"},"uri":"https:\/\/zotonic.com\/id\/1410","uri_template":"https:\/\/zotonic.com\/id\/:id","version":4164,"websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1410"}},"status":"ok"}