1"use strict";(globalThis.webpackChunkemailengine_temp=globalThis.webpackChunkemailengine_temp||[]).push([[6011],{12780(e,n,s){s.r(n),s.d(n,{assets:()=>o,contentTitle:()=>l,default:()=>h,frontMatter:()=>a,metadata:()=>i,toc:()=>d});const i=JSON.parse('{"id":"configuration/translations","title":"Translations","description":"Configure language settings for public-facing pages and contribute translations","source":"@site/docs/configuration/translations.md","sourceDirName":"configuration","slug":"/configuration/translations","permalink":"/docs/configuration/translations","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":7,"frontMatter":{"title":"Translations","sidebar_position":7,"description":"Configure language settings for public-facing pages and contribute translations"},"sidebar":"docsSidebar","previous":{"title":"Local IP Address Binding","permalink":"/docs/configuration/local-addresses"},"next":{"title":"Command Line Interface (CLI)","permalink":"/docs/configuration/cli"}}');var r=s(74848),t=s(28453);const a={title:"Translations",sidebar_position:7,description:"Configure language settings for public-facing pages and contribute translations"},l="Translations",o={},d=[{value:"Supported Languages",id:"supported-languages",level:2},{value:"Language Selection",id:"language-selection",level:2},{value:"Per-Request Language (Query Parameter)",id:"per-request-language-query-parameter",level:3},{value:"Per-Request Language (Header)",id:"per-request-language-header",level:3},{value:"Browser Language Detection",id:"browser-language-detection",level:3},{value:"Default Language (Server-Wide)",id:"default-language-server-wide",level:3},{value:"Pre-selecting Language in Hosted Authentication",id:"pre-selecting-language-in-hosted-authentication",level:3},{value:"What Gets Translated",id:"what-gets-translated",level:2},{value:"Public UI Pages",id:"public-ui-pages",level:3},{value:"API Validation Errors",id:"api-validation-errors",level:3},{value:"Contributing a translation",id:"contributing-a-translation",level:2},{value:"See Also",id:"see-also",level:2}];function c(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"translations",children:"Translations"})}),"\n",(0,r.jsx)(n.p,{children:"EmailEngine supports multiple languages for public-facing pages. This includes hosted authentication forms, error pages, unsubscribe pages, and other user-facing content."}),"\n",(0,r.jsx)(n.admonition,{title:"Admin Interface Not Translated",type:"note",children:(0,r.jsx)(n.p,{children:"The admin dashboard and configuration interface are only available in English. Translations apply only to public pages that end users interact with."})}),"\n",(0,r.jsx)(n.h2,{id:"supported-languages",children:"Supported Languages"}),"\n",(0,r.jsx)(n.p,{children:"EmailEngine includes translations for the following languages:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Language"}),(0,r.jsx)(n.th,{children:"Locale Code"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"English"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"en"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Estonian"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"et"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"French"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"fr"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"German"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"de"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Polish"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"pl"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Japanese"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ja"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Dutch"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"nl"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Swedish"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"sv"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Spanish"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"es"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Italian"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"it"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Turkish"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"tr"})})]})]})]}
1),"\n",(0,r.jsx)(n.p,{children:"Swedish, Spanish, Italian and Turkish were added in EmailEngine v2.81.0."}),"\n",(0,r.jsx)(n.h2,{id:"language-selection",children:"Language Selection"}),"\n",(0,r.jsx)(n.p,{children:"EmailEngine determines the language to use based on the following priority order:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Query parameter"})," - ",(0,r.jsx)(n.code,{children:"?locale=fr"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Custom header"})," - ",(0,r.jsx)(n.code,{children:"X-EE-Locale: fr"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Session cookie"})," - Stored from previous query/header selection"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Accept-Language header"})," - Browser's language preference"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Default locale"})," - Server-wide setting"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"per-request-language-query-parameter",children:"Per-Request Language (Query Parameter)"}),"\n",(0,r.jsxs)(n.p,{children:["Append ",(0,r.jsx)(n.code,{children:"locale=<code>"})," to the query string of any public page URL, for example a hosted authentication form URL returned by ",(0,r.jsx)(n.code,{children:"POST /v1/authentication/form"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",children:"https://emailengine.example.com/accounts/new?data=eyJhY2NvdW50Ijoi...&sig=Ah0z...&locale=fr\n"})}),"\n",(0,r.jsx)(n.p,{children:"When set via query parameter, the language selection is stored in a session cookie and persists until the browser session ends or a different language is selected."}),"\n",(0,r.jsx)(n.h3,{id:"per-request-language-header",children:"Per-Request Language (Header)"}),"\n",(0,r.jsxs)(n.p,{children:["Set the ",(0,r.jsx)(n.code,{children:"X-EE-Locale"})," header in your request. Here ",(0,r.jsx)(n.code,{children:"FORM_URL"})," is the ",(0,r.jsx)(n.code,{children:"url"})," returned by ",(0,r.jsx)(n.code,{children:"POST /v1/authentication/form"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'curl "$FORM_URL" \\\n -H "X-EE-Locale: de"\n'})}),"\n",(0,r.jsx)(n.p,{children:"Like the query parameter, this also sets a session cookie to persist the selection."}),"\n",(0,r.jsx)(n.h3,{id:"browser-language-detection",children:"Browser Language Detection"}),"\n",(0,r.jsxs)(n.p,{children:["If no explicit locale is set, EmailEngine uses the browser's ",(0,r.jsx)(n.code,{children:"Accept-Language"})," header to negotiate the best available language. For example, a browser sending ",(0,r.jsx)(n.code,{children:"Accept-Language: de-DE,de;q=0.9,en;q=0.8"})," would see German if available."]}),"\n",(0,r.jsx)(n.h3,{id:"default-language-server-wide",children:"Default Language (Server-Wide)"}),"\n",(0,r.jsx)(n.p,{children:"Set the server-wide default locale that applies when no other language preference is detected."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Via API:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'curl -X POST https://emailengine.example.com/v1/settings \\\n -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "locale": "fr"\n }\'\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Via Web Interface:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Open the EmailEngine admin dashboard"}),"\n",(0,r.jsxs)(n.li,{children:["Navigate to ",(0,r.jsx)(n.strong,{children:"Configuration"})," > ",(0,r.jsx)(n.strong,{children:"General"})]}),"\n",(0,r.jsxs)(n.li,{children:["Find the ",(0,r.jsx)(n.strong,{children:"Default Language"})," setting"]}),"\n",(0,r.jsx)(n.li,{children:"Select your preferred language from the dropdown"}),"\n",(0,r.jsxs)(n.li,{children:["Click ",(0,r.jsx)(n.strong,{children:"Save"})]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"pre-selecting-language-in-hosted-authentication",children:"Pre-selecting Language in Hosted Authentication"}),"\n",(0,r.jsx)(n.p,{children:"When generating authentication form URLs, you can include the locale parameter to display the form in a specific language:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'curl -X POST https://emailengine.example.com/v1/authentication/form \\\n -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "account": "user123",\n "redirectUrl": "https://myapp.com/settings"\n }\'\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Then append ",(0,r.jsx)(n.code,{children:"&locale=fr"})," to the returned ",(0,r.jsx)(n.code,{children:"url"})," before redirecting the user:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",children:"https://emailengine.example.com/accounts/new?data=eyJhY2NvdW50Ijoi...&sig=Ah0z...&locale=fr\n"})}),"\n",(0,r.jsx)(n.h2,{id:"what-gets-translated",children:"What Gets Translated"}),"\n",(0,r.jsx)(n.h3,{id:"public-ui-pages",children:"Public UI Pages"}),"\n",(0,r.jsx)(n.p,{children:"Translations apply to public-facing pages:"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Hosted Authentication Forms:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'Account type selection ("Choose your email account provider")'}),"\n",(0,r.jsx)(n.li,{children:'IMAP/SMTP configuration form labels and buttons ("Verify connection", "Save and continue")'}),"\n",(0,r.jsx)(n.li,{children:'Connection test results ("Couldn\'t connect to IMAP server", "Server response:")'}),"\n",(0,r.jsx)(n.li,{children:'Expired or invalid setup links ("Invalid or expired account setup URL")'}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Unsubscribe Pages:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Unsubscribe confirmation"}),"\n",(0,r.jsx)(n.li,{children:"Re-subscribe option"}),"\n",(0,r.jsx)(n.li,{children:"Status messages"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Error Pages:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'OAuth2 failures ("OAuth2 authentication failed") and missing-scope explanations'}),"\n",(0,r.jsx)(n.li,{children:'Connection errors ("Could not connect to server")'}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Redirect Pages:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'"Click here to continue" messages'}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"api-validation-errors",children:"API Validation Errors"}),"\n",(0,r.jsx)(n.p,{children:"API validation error messages are also translated. The same language selection mechanism applies to API requests:"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Triggers for API response translation:"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Method"}),(0,r.jsx)(n.th,{children:"Example"}),(0,r.jsx)(n.th,{children:"Persists"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Query parameter"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"POST /v1/account?locale=fr"})}),(0,r.jsx)(n.td,{children:"No"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Custom header"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"X-EE-Locale: fr"})}),(0,r.jsx)(n.td,{children:"No"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Accept-Language header"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Accept-Language: fr"})}),(0,r.jsx)(n.td,{children:"No"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Example - German validation error:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'curl -X POST https://emailengine.example.com/v1/account \\\n -H "Authorization: Bearer YOUR_TOKEN" \\\n -H "X-EE-Locale: de" \\\n -H "Content-Type: application/json" \\\n -d \'{}\'\n'})}),"\n",(0,r.jsx)(n.p,{children:"Response with German error messages:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "statusCode": 400,\n "error": "Bad Request",\n "message": "Ung\xfcltige Eingabe",\n "fields": [\n {\n "message": "\\"account\\" ist erforderlich",\n "key": "account"\n },\n {\n "message": "\\"name\\" ist erforderlich",\n "key": "name"\n }\n ]\n}\n'})}),"\n",(0,r.jsx)(n.admonition,{title:"Cookie Persistence",type:"note",children:(0,r.jsxs)(n.p,{children:["For API requests (paths starting with ",(0,r.jsx)(n.code,{children:"/v1/"}),") and for ",(0,r.jsx)(n.code,{children:"/health"}),", locale selection via query parameter or header does ",(0,r.jsx)(n.strong,{children:"not"})," set a session cookie. Each API request must explicitly specify the desired locale. Cookies are only set for UI page requests, and only from the query parameter or the ",(0,r.jsx)(n.code,{children:"X-EE-Locale"})," header, never from ",(0,r.jsx)(n.code,{children:"Accept-Language"}),"."]})}),"\n",(0,r.jsx)(n.h2,{id:"contributing-a-translation",children:"Contributing a translation"}),"\n",(0,r.jsxs)(n.p,{children:["The catalogs live in the ",(0,r.jsx)(n.a,{href:"https://github.com/postalsys/emailengine/tree/master/translations",children:(0,r.jsx)(n.code,{children:"translations/"})})," directory of the EmailEngine repository. ",(0,r.jsx)(n.code,{children:"messages.pot"}
1)," is the template listing every translatable string, each ",(0,r.jsx)(n.code,{children:"<locale>.po"})," holds one language, and the ",(0,r.jsx)(n.code,{children:"<locale>.mo"})," beside it is the compiled form EmailEngine loads at runtime. A locale is served only if it is also listed in ",(0,r.jsx)(n.code,{children:"locales.json"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"To add a language:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["Create a new catalog in ",(0,r.jsx)(n.a,{href:"https://poedit.net/",children:"Poedit"})," from ",(0,r.jsx)(n.code,{children:"messages.pot"})," (",(0,r.jsx)(n.strong,{children:"Update from POT"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:["Translate the strings and save the file as ",(0,r.jsx)(n.code,{children:"<locale>.po"}),"; Poedit compiles the ",(0,r.jsx)(n.code,{children:".mo"})," alongside it"]}),"\n",(0,r.jsxs)(n.li,{children:["Open a pull request against the repository, or send the ",(0,r.jsx)(n.code,{children:".po"})," file to ",(0,r.jsx)(n.a,{href:"mailto:[email protected]",children:"[email protected]"})]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"README.md"}
1)," in the same directory describes the equivalent GNU gettext command-line workflow, including how to refresh an existing catalog after strings change."]}),"\n",(0,r.jsxs)(n.p,{children:["Validation error messages come from a separate package. Their translations are maintained in the ",(0,r.jsx)(n.a,{href:"https://github.com/postalsys/joi-messages/tree/master/translations",children:"joi-messages"})," repository."]}),"\n",(0,r.jsx)(n.h2,{id:"see-also",children:"See Also"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/docs/accounts/hosted-authentication",children:"Hosted Authentication"})," - The public forms that translations apply to"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/docs/sending/deliverability/suppression-lists",children:"Virtual Mailing Lists"})," - The hosted unsubscribe page, another localized public page"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/docs/api-reference/#error-handling",children:"API Reference Overview"})," - The shape of the validation errors that are translated"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/docs/configuration/settings",children:"Configuration Options"})," - The ",(0,r.jsx)(n.code,{children:"locale"})," setting among all the others"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},28453(e,n,s){s.d(n,{R:()=>a,x:()=>l});var i=s(96540);const r={},t=i.createContext(r);function a(e){const n=i.useContext(t);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(t.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.