livecode-static/docs/assets/js/59d3e69b.f0958acd.js
2025-06-12 09:37:26 +08:00

1 line
32 KiB
JavaScript

"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([["6484"],{9342:function(e,n,s){s.r(n),s.d(n,{default:()=>h,frontMatter:()=>a,metadata:()=>t,assets:()=>o,toc:()=>d,contentTitle:()=>l});var t=JSON.parse('{"id":"contribution/i18n","title":"i18n","description":"This document provides a guide on how to contribute to the translation of the app.","source":"@site/docs/contribution/i18n.mdx","sourceDirName":"contribution","slug":"/contribution/i18n","permalink":"/docs/contribution/i18n","draft":false,"unlisted":false,"editUrl":"https://github.com/live-codes/livecodes/tree/develop/docs/docs/contribution/i18n.mdx","tags":[],"version":"current","frontMatter":{}}'),i=s("5893"),r=s("65");let a={},l="i18n",o={},d=[{value:"For Translators",id:"for-translators",level:2},{value:"Contribute to Translation",id:"contribute-to-translation",level:3},{value:"Add a New Language",id:"add-a-new-language",level:3},{value:"Technical Overview",id:"technical-overview",level:2},{value:"For Developers",id:"for-developers",level:2},{value:"Strings",id:"strings",level:3},{value:"Element-level Translation (HTML Files)",id:"element-level-translation-html-files",level:4},{value:"Keys",id:"keys",level:5},{value:"Value",id:"value",level:5},{value:"Props",id:"props",level:5},{value:"Abstract HTML Tags",id:"abstract-html-tags",level:6},{value:"Interpolation",id:"interpolation",level:5},{value:"String-level Translation (TypeScript Files)",id:"string-level-translation-typescript-files",level:4},{value:"Scripts",id:"scripts",level:3},{value:"For Maintainers",id:"for-maintainers",level:2},{value:"Workflow",id:"workflow",level:3},{value:"No-Source Update",id:"no-source-update",level:4},{value:"Source Update",id:"source-update",level:4},{value:"Minor Fixes / Updates",id:"minor-fixes--updates",level:4},{value:"Github Actions (CI)",id:"github-actions-ci",level:3},{value:"Hashing and Cache",id:"hashing-and-cache",level:3},{value:"For Those Who Forked the Repo",id:"for-those-who-forked-the-repo",level:2},{value:"Secrets and Variables Checklist",id:"secrets-and-variables-checklist",level:3},{value:"Repository Secrets",id:"repository-secrets",level:4},{value:"Repository Variables",id:"repository-variables",level:4}];function c(e){let n={a:"a",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",h5:"h5",h6:"h6",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"i18n",children:"i18n"})}),"\n",(0,i.jsx)(n.p,{children:"This document provides a guide on how to contribute to the translation of the app."}),"\n",(0,i.jsx)(n.h2,{id:"for-translators",children:"For Translators"}),"\n",(0,i.jsx)(n.p,{children:"Translators are responsible for translating the source texts on Lokalise."}),"\n",(0,i.jsx)(n.h3,{id:"contribute-to-translation",children:"Contribute to Translation"}),"\n",(0,i.jsxs)(n.p,{children:["Please visit the ",(0,i.jsx)(n.a,{href:"https://app.lokalise.com/public/34958094667a72e9454592.95108106/",children:"Lokalise project page"})," to contribute to the translation of LiveCodes. You might find the ",(0,i.jsx)(n.a,{href:"https://docs.lokalise.com/en/articles/2967175-onboarding-guide-for-translators",children:"Onboarding Guide for Translators"})," on Lokalise helpful."]}),"\n",(0,i.jsx)(n.h3,{id:"add-a-new-language",children:"Add a New Language"}),"\n",(0,i.jsxs)(n.p,{children:["If you find that the language you want to translate to is not available on Lokalise, please kindly ",(0,i.jsx)(n.a,{href:"https://github.com/live-codes/livecodes/issues/new?template=i18n_request.yml",children:"raise an issue"})," in the repository with further details about the language you want to add."]}),"\n",(0,i.jsx)(n.h2,{id:"technical-overview",children:"Technical Overview"}),"\n",(0,i.jsxs)(n.p,{children:["The i18n framework ",(0,i.jsx)(n.a,{href:"https://www.i18next.com/",children:(0,i.jsx)(n.code,{children:"i18next"})})," and the online translation collaboration platform ",(0,i.jsx)(n.a,{href:"https://lokalise.com/",children:"Lokalise"})," are used to manage the i18n of LiveCodes."]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:"It is recommended to read the related documentation of the above tools before continuing."}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["The i18n workflow is designed to be ",(0,i.jsx)(n.strong,{children:"source-based"}),", which means that the source texts are extracted from the codebase and uploaded to Lokalise for translation. After the translation is complete, the translated texts are integrated back into the codebase. For more details, please refer to the ",(0,i.jsx)(n.a,{href:"#workflow",children:"Workflow"})," section."]}),"\n",(0,i.jsxs)(n.p,{children:["Two types of strings mentioned in the ",(0,i.jsx)(n.a,{href:"#strings",children:"Strings"})," section are considered as the ",(0,i.jsx)(n.strong,{children:"source texts"}),", and English is the ",(0,i.jsx)(n.strong,{children:"source language"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Extracted source texts are stored in two forms under ",(0,i.jsx)(n.code,{children:"src/livecodes/i18n/locales/en"}),":"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:".ts"})," files: used by the app to load the source texts and provide type-safety for TypeScript"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:".lokalise.json"})," files: used to upload the source texts to Lokalise"]}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["These files are generated and kept in sync with each other by the ",(0,i.jsx)(n.code,{children:"i18n-export"})," npm script. See the ",(0,i.jsx)(n.a,{href:"#scripts",children:"Scripts"})," section for more details."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Other directories under ",(0,i.jsx)(n.code,{children:"src/livecodes/i18n/locales"})," are used to store the translated texts in other target languages, which only contain ",(0,i.jsx)(n.code,{children:".ts"})," files."]}),"\n",(0,i.jsx)(n.h2,{id:"for-developers",children:"For Developers"}),"\n",(0,i.jsx)(n.p,{children:"Developers are responsible for implementing new features or making changes to the existing codebase. When adding new strings or modifying existing strings, developers should ensure that the newly-edited strings are properly extracted and saved."}),"\n",(0,i.jsx)(n.h3,{id:"strings",children:"Strings"}),"\n",(0,i.jsxs)(n.p,{children:["Strings that need to be translated are located in both ",(0,i.jsx)(n.code,{children:"src/livecodes/html/*.html"})," and other ",(0,i.jsx)(n.code,{children:".ts"})," files in ",(0,i.jsx)(n.code,{children:"src/livecodes"})," (mostly in ",(0,i.jsx)(n.code,{children:"src/livecodes/UI/"}),"). These two different types of files, which also represent two types of translation methods, are handled differently in the i18n workflow:"]}),"\n",(0,i.jsx)(n.h4,{id:"element-level-translation-html-files",children:"Element-level Translation (HTML Files)"}),"\n",(0,i.jsxs)(n.p,{children:["In these files, strings are wrapped inside HTML elements with ",(0,i.jsx)(n.code,{children:"data-i18n"})," attribute and two optional attributes (",(0,i.jsx)(n.code,{children:"data-i18n-prop"}),", ",(0,i.jsx)(n.code,{children:"data-i18n-interpolation"}),"). For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-html",children:'<div class="modal-title" data-i18n="assets.heading">Assets</div>\n\n<input\n id="search-assets"\n type="text"\n placeholder="Search"\n data-i18n="assets.search"\n data-i18n-prop="placeholder"\n/>\n\n<div class="description" data-i18n="backup.backup.desc" data-i18n-prop="innerHTML">\n Backup LiveCodes data, so that it can be later restored on this or other devices. <br />\n Please visit the\n <a href="{{DOCS_BASE_URL}}features/backup-restore" target="_blank" rel="noopener"\n >documentations</a\n >\n for details.\n</div>\n'})}),"\n",(0,i.jsx)(n.h5,{id:"keys",children:"Keys"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"data-i18n"})," attribute is used to specify the ",(0,i.jsx)(n.strong,{children:"key"})," of the string and is a ",(0,i.jsx)(n.strong,{children:"period-separated"})," string with each part being a ",(0,i.jsx)(n.strong,{children:"lowerCamelCase"})," word."]}),"\n",(0,i.jsx)(n.h5,{id:"value",children:"Value"}),"\n",(0,i.jsxs)(n.p,{children:["The value of the corresponding attribute of the element is used as the ",(0,i.jsx)(n.strong,{children:"default / fallback value"})," of the string."]}),"\n",(0,i.jsx)(n.h5,{id:"props",children:"Props"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"data-i18n-prop"})," attribute is a ",(0,i.jsx)(n.strong,{children:"space-separated list"})," of properties that should be translated. If it is not present, the string will be translated as the ",(0,i.jsx)(n.code,{children:"textContent"}),' of the element. When two or more properties are specified, a "full key" (',(0,i.jsx)(n.code,{children:"<key>.<property>"}),", or ",(0,i.jsx)(n.code,{children:"<key>#<property>"})," on Lokalise) will be used to identify the string. For example, for the following element:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-html",children:'<div title="This is a title" data-i18n="key.to.title" data-i18n-prop="title textContent">\n This is a content\n</div>\n'})}),"\n",(0,i.jsxs)(n.p,{children:["The string will be identified as ",(0,i.jsx)(n.code,{children:"key.to.title#title"})," and ",(0,i.jsx)(n.code,{children:"key.to.title#textContent"})," on Lokalise."]}),"\n",(0,i.jsx)(n.h6,{id:"abstract-html-tags",children:"Abstract HTML Tags"}),"\n",(0,i.jsxs)(n.p,{children:["When the ",(0,i.jsx)(n.code,{children:"data-i18n-prop"})," attribute is ",(0,i.jsx)(n.code,{children:"innerHTML"}),", HTML tags inside the value will be abstracted during exporting, making the final source texts more readable. For example, the following value:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-html",children:'<div class="description" data-i18n="backup.backup.desc" data-i18n-prop="innerHTML">\n Backup LiveCodes data, so that it can be later restored on this or other devices. <br />\n Please visit the\n <a href="{{DOCS_BASE_URL}}features/backup-restore" target="_blank" rel="noopener"\n >documentations</a\n >\n for details.\n</div>\n'})}),"\n",(0,i.jsx)(n.p,{children:"will be abstracted to:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-html",children:"Backup LiveCodes data, so that it can be later restored on this or other devices. <1></1> Please visit the <2>documentations</2> for details.\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Although overriding specified attributes or adding new ones to the corresponding element is supported (",(0,i.jsx)(n.code,{children:'<2 href="https://example.org">documentations</2>'}),"), it is not recommended to do so unless necessary (e.g., docs link for different languages)."]}),"\n",(0,i.jsx)(n.h5,{id:"interpolation",children:"Interpolation"}),"\n",(0,i.jsxs)(n.p,{children:["Interpolation is used to insert dynamic content into the string. The ",(0,i.jsx)(n.code,{children:"data-i18n-interpolation"})," attribute is a ",(0,i.jsx)(n.strong,{children:"JSON object string"})," that contains the key-value pairs of the dynamic content. For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-html",children:'<div class="share-encoded-url-expiry">\n <span class="{{warnClass}}" data-i18n="share.characters">{{urlLength}} characters</span\n ><a href="#" data-i18n="share.shortURL">Get short URL</a>\n</div>\n'})}),"\n",(0,i.jsxs)(n.p,{children:["In related TypeScript files, the ",(0,i.jsx)(n.code,{children:"data-i18n-interpolation"})," attribute should be set as follows:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"charactersSpan.dataset.i18nInterpolation = JSON.stringify({ urlLength });\n"})}),"\n",(0,i.jsx)(n.h4,{id:"string-level-translation-typescript-files",children:"String-level Translation (TypeScript Files)"}),"\n",(0,i.jsxs)(n.p,{children:["In these files, strings are wrapped inside ",(0,i.jsx)(n.code,{children:"window.deps.translateString(key, value, interpolation)"})," function calls. For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"window.deps.translateString(\n 'namespace:file.key1.subkey1',\n 'default <strong>value</strong>, {{interpol}}',\n {\n isHTML: true,\n interpol: 'abc',\n },\n);\n\nwindow.deps.translateString('core.login.successWithName', 'Logged in as: {{name}}', {\n name: displayName,\n});\n"})}),"\n",(0,i.jsx)(n.p,{children:"The function is completely type-safe:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The first argument is ",(0,i.jsx)(n.code,{children:"key"}),", which is validated with valid keys in ",(0,i.jsx)(n.code,{children:"i18n/locales/en/"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["The second argument is ",(0,i.jsx)(n.code,{children:"value"}),". With the ",(0,i.jsx)(n.code,{children:"key"})," provided, the type of ",(0,i.jsx)(n.code,{children:"value"})," will be narrowed down to the value in the translation file to ensure all occurrences of the key have the same value. If this is an HTML string (with the ",(0,i.jsx)(n.code,{children:"isHTML"})," attribute set to ",(0,i.jsx)(n.code,{children:"true"}),"), the value will automatically be abstracted when exporting the translation."]}),"\n",(0,i.jsxs)(n.li,{children:["The third argument is ",(0,i.jsx)(n.code,{children:"interpolation"}),", which could be omitted when the value doesn't contain any interpolation. Otherwise, it should be an object whose attributes are inferred from the ",(0,i.jsx)(n.code,{children:"value"}),". Moreover, an additional ",(0,i.jsx)(n.code,{children:"isHTML"})," boolean attribute is added to indicate whether the ",(0,i.jsx)(n.code,{children:"value"})," contains HTML tags and should be abstracted when exporting the translation."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"To provide better readability and maintainability, only string-level translation will be used in .ts files when it comes to dynamic content. For static content, element-level translation is still the best choice."}),"\n",(0,i.jsx)(n.h3,{id:"scripts",children:"Scripts"}),"\n",(0,i.jsx)(n.p,{children:"Several npm scripts are available to facilitate the i18n workflow:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(7413).Z+"",children:(0,i.jsx)(n.code,{children:"vscode-intellisense"})}),": Generates the ",(0,i.jsx)(n.code,{children:"html.html-data.json"})," file in the ",(0,i.jsx)(n.code,{children:".vscode"})," folder to enhance intellisense for the ",(0,i.jsx)(n.code,{children:"data-i18n"})," attribute in HTML files in VSCode."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(2565).Z+"",children:(0,i.jsx)(n.code,{children:"i18n-export"})}),": Extracts source texts from the codebase and generates ",(0,i.jsx)(n.code,{children:".ts"})," and ",(0,i.jsx)(n.code,{children:".lokalise.json"})," files under ",(0,i.jsx)(n.code,{children:"src/livecodes/i18n/locales/en"}),".","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"--save-tmp"})," flag can be used to save the extracted source texts to ",(0,i.jsx)(n.code,{children:"i18n/locales/tmp"})," instead of ",(0,i.jsx)(n.code,{children:"en"})," for debugging purposes."]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsxs)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(6114).Z+"",children:[(0,i.jsx)(n.code,{children:"i18n-update-push"})," / ",(0,i.jsx)(n.code,{children:"i18n-upload.mjs"})]}),": ",(0,i.jsx)(n.strong,{children:"(Only used in CI and should not be run locally)"})," Pushes the source texts in ",(0,i.jsx)(n.code,{children:".lokasile.json"})," files to Lokalise.","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"<branch>"})," argument is required to specify the branch to push to."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"--force"})," flag can be used to skip the check for environment variable ",(0,i.jsx)(n.code,{children:"CI"})," and allow running the script locally."]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsxs)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(2379).Z+"",children:[(0,i.jsx)(n.code,{children:"i18n-update-pull"})," / ",(0,i.jsx)(n.code,{children:"i18n-import.mjs"})]}),": ",(0,i.jsx)(n.strong,{children:"(Only used in CI and should not be run locally)"})," Pulls the translated texts from Lokalise and updates the ",(0,i.jsx)(n.code,{children:".ts"})," files under ",(0,i.jsx)(n.code,{children:"src/livecodes/i18n/locales"}),". ",(0,i.jsx)(n.em,{children:"Outdated translation will be deprecated during import."}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"<branch>"})," argument is required to specify the branch to pull from."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"--force"})," flag can be used to skip the check for environment variable ",(0,i.jsx)(n.code,{children:"CI"})," and allow running the script locally."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"--local"})," flag can be used to let the script use local resources in the directory defined by the ",(0,i.jsx)(n.code,{children:"LOKALISE_TEMP"})," environment variable instead of fetching from Lokalise."]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(64).Z+"",children:(0,i.jsx)(n.code,{children:"i18n-exclude"})}),": ",(0,i.jsx)(n.strong,{children:"(Only used in other scripts and should not be run locally)"})," Excludes all other i18n locales except for English from type checking, as they might stay outdated and cause errors.","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"<phase>"})," argument is required to specify whether the script is to exclude files or revert the exclusion. Valid values are ",(0,i.jsx)(n.code,{children:"pre"})," and ",(0,i.jsx)(n.code,{children:"post"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["This script only works when environment variable ",(0,i.jsx)(n.code,{children:"BUILD_INCLUDE_LOCALES"})," is ",(0,i.jsx)(n.strong,{children:"NOT"})," set to ",(0,i.jsx)(n.code,{children:"true"}),"."]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(9173).Z+"",children:(0,i.jsx)(n.code,{children:"i18n-lokalise-json"})}),": Generates ",(0,i.jsx)(n.code,{children:".lokalise.json"})," files from codebase for manually authoring translations on Lokalise.","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Arguments to the script are required to specify the languages that the ",(0,i.jsx)(n.code,{children:".lokalise.json"})," files are generated from."]}),"\n",(0,i.jsxs)(n.li,{children:["You can also use ",(0,i.jsx)(n.code,{children:"all"})," to generate ",(0,i.jsx)(n.code,{children:".lokalise.json"})," files for all languages."]}),"\n",(0,i.jsxs)(n.li,{children:["This script ",(0,i.jsx)(n.strong,{children:"should not"})," be used for the source language English. Use ",(0,i.jsx)(n.code,{children:"i18n-export"})," instead."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Please run ",(0,i.jsx)(n.code,{children:"i18n-export"})," before pushing changes to the codebase to ensure that the source texts are up-to-date."]}),"\n",(0,i.jsx)(n.h2,{id:"for-maintainers",children:"For Maintainers"}),"\n",(0,i.jsx)(n.p,{children:"Maintainers are responsible for managing the i18n workflow and ensuring the quality of translations."}),"\n",(0,i.jsx)(n.h3,{id:"workflow",children:"Workflow"}),"\n",(0,i.jsx)(n.p,{children:"We consider the i18n process to consist of two parts:"}),"\n",(0,i.jsx)(n.h4,{id:"no-source-update",children:"No-Source Update"}),"\n",(0,i.jsx)(n.p,{children:"This means there are no changes to the source code/texts, only translations are updated. Adding new languages or updating existing translations are examples of this part."}),"\n",(0,i.jsxs)(n.p,{children:["In such cases, there is a scheduled workflow ",(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(298).Z+"",children:(0,i.jsx)(n.code,{children:"i18n-update-scheduled"})})," to handle this. The workflow will sync from the ",(0,i.jsx)(n.code,{children:"master"})," branch on Lokalise to the ",(0,i.jsx)(n.code,{children:"i18n/develop"})," branch on the codebase, then automatically create a PR if there are any changes."]}),"\n",(0,i.jsx)(n.p,{children:"Basically, maintainers only need to focus on the following for this part:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Reviewing PRs created by the ",(0,i.jsx)(n.code,{children:"i18n-update-scheduled"})," workflow"]}),"\n",(0,i.jsxs)(n.li,{children:["Do merging on Lokalise after they consider the translation for a specific feature is ready, before commenting ",(0,i.jsx)(n.code,{children:".i18n-update-pull"})," to trigger the ",(0,i.jsx)(n.code,{children:"i18n-update-pull"})," workflow"]}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"source-update",children:"Source Update"}),"\n",(0,i.jsx)(n.p,{children:"This means new changes are made to the source code/texts. In this case, maintainers should follow the steps below:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["Developers work on the source code, developing new features or modifying existing ones. They use ",(0,i.jsx)(n.code,{children:"data-i18n"})," and ",(0,i.jsx)(n.code,{children:"window.deps.translateString"})," to mark the strings that need to be translated."]}),"\n",(0,i.jsxs)(n.li,{children:["Once a new feature or version is ready, developers run ",(0,i.jsx)(n.code,{children:"npm run i18n-export"})," to extract and update all marked strings, then export them to ",(0,i.jsx)(n.code,{children:".lokalise.json"})," and ",(0,i.jsx)(n.code,{children:".ts"})," files."]}),"\n",(0,i.jsx)(n.li,{children:"Developers commit and push the changes to the repository. A feature PR is created and reviewed, and related tests, checks, and manual review could be carried out."}),"\n",(0,i.jsxs)(n.li,{children:["After the PR is merged, an auto-generated comment will notify maintainers to comment ",(0,i.jsx)(n.code,{children:".i18n-update-push"})," to trigger the ",(0,i.jsx)(n.code,{children:"i18n-update-push"})," workflow when they think it is ready."]}),"\n",(0,i.jsxs)(n.li,{children:["Maintainers comment ",(0,i.jsx)(n.code,{children:".i18n-update-push"})," to trigger the ",(0,i.jsx)(n.code,{children:"i18n-update-push"})," workflow. The workflow will create a new branch named ",(0,i.jsx)(n.code,{children:"i18n/<owner>/<head-branch>"}),", run ",(0,i.jsx)(n.code,{children:"npm run i18n-export"})," again to ensure the source texts are up-to-date, and push the changes. Then, it will push the changes to the ",(0,i.jsx)(n.code,{children:"i18n/<owner>/<head-branch>"})," branch on Lokalise."]}),"\n",(0,i.jsx)(n.li,{children:"Translators can start translating the texts on Lokalise."}),"\n",(0,i.jsxs)(n.li,{children:["Once the translation is complete, maintainers can comment ",(0,i.jsx)(n.code,{children:".i18n-update-pull"})," to trigger the ",(0,i.jsx)(n.code,{children:"i18n-update-pull"})," workflow. The workflow will pull the translated texts from Lokalise, update the ",(0,i.jsx)(n.code,{children:".ts"})," files under ",(0,i.jsx)(n.code,{children:"src/livecodes/i18n/locales"}),", and commit the changes to the ",(0,i.jsx)(n.code,{children:"i18n/<owner>/<head-branch>"})," branch. Then, it will create a PR to merge the changes back to the default branch ",(0,i.jsx)(n.code,{children:"develop"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["Maintainers should perform a final review on the i18n PR and merge it if everything is fine. Meanwhile, a merging from the ",(0,i.jsx)(n.code,{children:"i18n/<owner>/<head-branch>"})," to ",(0,i.jsx)(n.code,{children:"master"})," should also be done to keep the ",(0,i.jsx)(n.code,{children:"master"})," branch on Lokalise up-to-date."]}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"minor-fixes--updates",children:"Minor Fixes / Updates"}),"\n",(0,i.jsx)(n.p,{children:"Sometimes there is already an ongoing main prerelease branch with many features being developed and translated on Lokalise, and a minor fix or update to the prerelease branch is needed. In this case, maintainers should follow the steps below:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["Switch to ",(0,i.jsx)(n.code,{children:"develop"})," branch."]}),"\n",(0,i.jsxs)(n.li,{children:["Do ",(0,i.jsx)(n.code,{children:"i18n-export"})," and upload corresponding ",(0,i.jsx)(n.code,{children:".lokalise.json"})," to the prerelease branch of the Lokalise project through web UI."]}),"\n",(0,i.jsxs)(n.li,{children:["Affected entries will be updated and ",(0,i.jsxs)(n.a,{href:"https://docs.lokalise.com/en/articles/3684557-translation-statuses-translated-verified-reviewed-and-completed#verified-and-unverified",children:["marked as ",(0,i.jsx)(n.code,{children:"unverified"})]}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Provide correct translations in other languages on Lokalise."}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Here we do not want an extra i18n branch for simplicity, nor need to pull from Lokalise as we always consider English source strings from codebase as the latest version and do not recommend modifying them on Lokalise directly."}),"\n",(0,i.jsx)(n.h3,{id:"github-actions-ci",children:"Github Actions (CI)"}),"\n",(0,i.jsx)(n.p,{children:"Four i18n-related workflows are set up in the repository:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(3601).Z+"",children:(0,i.jsx)(n.code,{children:"i18n-update-notify"})}),": Creates a comment on merged PRs to notify maintainers to trigger the ",(0,i.jsx)(n.code,{children:"i18n-update-push"})," workflow."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(5207).Z+"",children:(0,i.jsx)(n.code,{children:"i18n-update-push"})}),": Creates a new branch named ",(0,i.jsx)(n.code,{children:"i18n/<owner>/<head-branch>"}),", runs ",(0,i.jsx)(n.code,{children:"npm run i18n-export"})," again to ensure the source texts are up-to-date, pushes the changes on git, then pushes the changes to the ",(0,i.jsx)(n.code,{children:"i18n/<owner>/<head-branch>"})," branch on Lokalise."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(3529).Z+"",children:(0,i.jsx)(n.code,{children:"i18n-update-pull"})}),": Pulls the translated texts from Lokalise, updates the ",(0,i.jsx)(n.code,{children:".ts"})," files under ",(0,i.jsx)(n.code,{children:"src/livecodes/i18n/locales"}),", commits the changes to the ",(0,i.jsx)(n.code,{children:"i18n/<owner>/<head-branch>"})," branch, then creates a PR to merge the changes back to the default branch ",(0,i.jsx)(n.code,{children:"develop"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(298).Z+"",children:(0,i.jsx)(n.code,{children:"i18n-update-scheduled"})}),": Syncs between the ",(0,i.jsx)(n.code,{children:"master"})," branch on Lokalise and the ",(0,i.jsx)(n.code,{children:"i18n/develop"})," branch on the codebase, then automatically creates a PR if there are any changes."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"hashing-and-cache",children:"Hashing and Cache"}),"\n",(0,i.jsxs)(n.p,{children:["After production build, ",(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(8802).Z+"",children:"file hashes are added"})," to all files in ",(0,i.jsx)(n.code,{children:"build/livecodes/"})," directory. The hash is the checksum of the file content. So if the file content does not change, it will get the same hash across builds."]}),"\n",(0,i.jsxs)(n.p,{children:["This assumes that all files to be hashed are in that directory (without nesting), and that they are referenced in code using placeholders like ",(0,i.jsx)(n.code,{children:"{{hash:file-name.js}}"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"const mod = await import(baseUrl + '{{hash:file-name.js}}');\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Then, these files are ",(0,i.jsx)(n.a,{href:"https://github.com/live-codes/livecodes/tree/develop/src/_headers",children:"aggressively cached"})," (for 1 year)."]}),"\n",(0,i.jsx)(n.p,{children:"So, any file that has not changed will continue to be served from cache even for later releases."}),"\n",(0,i.jsx)(n.p,{children:"File hashing is also applied to translation files during build, by auto-generating a path loader file that contains hard-coded hash placeholders for each translation file."}),"\n",(0,i.jsx)(n.h2,{id:"for-those-who-forked-the-repo",children:"For Those Who Forked the Repo"}),"\n",(0,i.jsxs)(n.p,{children:["This repository is utilizing ",(0,i.jsx)(n.a,{href:"https://github.com/apps/livecodes-ci",children:"LiveCodes CI"})," Github App to ensure the i18n workflow functions properly."]}),"\n",(0,i.jsxs)(n.p,{children:["For forked repositories, maintainers should set up their own Lokalise project and Github App (see ",(0,i.jsx)(n.a,{href:"https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.mdx#authenticating-with-github-app-generated-tokens",children:"here"}),") to handle the i18n workflow. Changes to related workflow files are necessary."]}),"\n",(0,i.jsx)(n.h3,{id:"secrets-and-variables-checklist",children:"Secrets and Variables Checklist"}),"\n",(0,i.jsx)(n.h4,{id:"repository-secrets",children:"Repository Secrets"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.code,{children:"LOKALISE_API_TOKEN"})}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.code,{children:"CI_APP_ID"})}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.code,{children:"CI_APP_PRIVATE_KEY"})}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"repository-variables",children:"Repository Variables"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.code,{children:"LOKALISE_PROJECT_ID"})}),"\n"]})]})}function h(e={}){let{wrapper:n}={...(0,r.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},3601:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/i18n-update-notify-a3777449a43532d607ba41185a4d7760.yml"},3529:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/i18n-update-pull-d89075850c83dbf05af9b0d6b67c1d1e.yml"},5207:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/i18n-update-push-0ccaf936f8b7727f7a8e5c8a4a944707.yml"},298:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/i18n-update-scheduled-fc9b46482ca124aee2d68a17c3a1b69f.yml"},8802:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/hash-9dd0148ddb2f39842f39f389ec4236cc.js"},64:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/i18n-exclude-e5d517ec6b92591b6019979892ee8d93.js"},2565:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/i18n-export-83977ebfdbe21c25c30819abc6321af1.js"},7413:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/vscode-intellisense-9573a05c29631b61eab8c1ea3da9c50d.js"},65:function(e,n,s){s.d(n,{Z:function(){return l},a:function(){return a}});var t=s(7294);let i={},r=t.createContext(i);function a(e){let n=t.useContext(r);return t.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(i):e.components||i:a(e.components),t.createElement(r.Provider,{value:n},e.children)}},2379:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/i18n-import-28359d2898f8ffcd1048c32f5d6a3401.mjs"},9173:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/i18n-lokalise-json-331642ee9c64647d271c8928cd4d8880.mjs"},6114:function(e,n,s){s.d(n,{Z:function(){return t}});let t=s.p+"assets/files/i18n-upload-6d83a823535487ee2d3034a686d86062.mjs"}}]);