打包好的livecode,版本v-46

This commit is contained in:
yangxin
2025-06-11 22:23:49 +08:00
commit 1214258379
1323 changed files with 133464 additions and 0 deletions

View File

@@ -0,0 +1 @@
const e=require("fs"),t=require("crypto"),i=async({devMode:i=!1,buildDir:r="build/livecodes/",entryPoint:a="index.js",patchFiles:l=["build/index.html"],hashPattern:s=/{{hash:([\w\.-]+)}}/g}={})=>{let n=["js","css","html","svg","ico","png","json"],o=async t=>(await e.promises.readdir(t)).filter(i=>!e.statSync(t+i).isDirectory()).filter(e=>n.some(t=>e.endsWith("."+t))),f=async e=>{let t=[];for(let i of e)(await o(i)).forEach(e=>{t.push(i+e)});return t},w=async()=>{for(let t of[...l,...await f([r])]){let i=(await e.promises.readFile(t,"utf8")).replace(new RegExp(s),(e,t)=>t);await e.promises.writeFile(t,i,"utf8")}};if(i)return w();let c=(e,t)=>{let i=n.find(t=>e.endsWith("."+t));return i&&(e=u(e).replace(`.${i}`,`.${t}.${i}`)),e},u=e=>{let t=e.split(".");return e.length<35||t.length<3?e:t.filter((e,t)=>32!==e.length||0===t).join(".")},p=e=>t.createHash("md5").update(e).digest("hex"),d={},h=async t=>{if(d[t])return;d[t]="waiting";let a=await e.promises.readFile(r+t,"utf8").catch(e=>{if(i)return"";throw e});for(let e of a.matchAll(new RegExp(s))){let t=e[1];t&&!d[t]&&await h(t)}let l=a.replace(new RegExp(s),(e,t)=>d[t]);if(i){d[t]=t,await e.promises.writeFile(r+t,l,"utf8");return}let n=c(t,p(l));d[t]=n,await e.promises.writeFile(r+n,l,"utf8")};for(let t of(await h(a),Object.keys(d)))d[t]!==t&&await e.promises.unlink(r+t).catch(e=>{if(!i)throw e});for(let t of l){let i=(await e.promises.readFile(t,"utf8")).replace(new RegExp(s),(e,t)=>d[t]);await e.promises.writeFile(t,i,"utf8")}};module.exports={applyHash:i},require.main===module&&i();

View File

@@ -0,0 +1,5 @@
const e=require("fs"),i=require("path"),r=`// @ts-nocheck
// This comment is added by i18n-exclude script and should be automatically removed after build.
// If you see this comment in the file, it means there is something wrong during the build process.
`,t=i.resolve("src/livecodes/i18n/locales");require.main===module&&(()=>{if("true"===process.env.BUILD_INCLUDE_LOCALES)return;let s=process.argv[2];console.log(`Running i18n-exclude in ${s} phase`),e.readdirSync(t,{withFileTypes:!0}).filter(e=>e.isDirectory()&&"en"!==e.name).map(e=>i.join(t,e.name)).forEach(t=>{for(let n of e.readdirSync(t).filter(e=>e.endsWith(".ts")).map(e=>i.join(t,e))){let i=e.readFileSync(n,"utf8");"pre"===s?i.startsWith(r)||(i=r+i):"post"===s&&(i=i.replace(r,"")),e.writeFileSync(n,i,"utf8")}})})(),module.exports={TS_NOCHECK:r};

View File

@@ -0,0 +1,18 @@
const e=require("fs"),t=require("path"),a=require("jsdom"),r=require("prettier"),n=require("@babel/core"),s=require("@babel/parser"),i=require("../package.json"),o=t.resolve("src/livecodes/i18n/locales/tmp"),l=t.resolve("src/livecodes/i18n/locales/en"),u=t.resolve("src/livecodes"),c=i.prettier,p="// ATTENTION: This file is auto-generated from source code. Do not edit manually!",d={translation:{},"language-info":{}},m={translation:{},"language-info":{}},g=(e,t=2)=>JSON.stringify(e,(e,t)=>t instanceof Object&&!(t instanceof Array)?Object.keys(t).sort().reduce((e,a)=>(e[a]=t[a],e),{}):t,t),h=async(a,n)=>{let s="translation"===a?"translation":"languageInfo",i=`${p}
import type { I18nTranslationTemplate } from '../models';
// This is used as a template for other translations.
// Other translations should be typed like this:
// const ${s}: ${"translation"===a?"I18nTranslation":"I18nLangInfoTranslation"} = { /* translation here */ };
// Since we allow nested objects, it is important to distinguish I18nTranslationTemplate from I18nAttributes.
// In view of this, properties declared in I18nAttributes (and those attributes might be used in future) shall not be used as a nested key.
const ${s} = ${g(d[a])} as const satisfies I18nTranslationTemplate;
export default ${s};
`,u=await r.format(i,{parser:"typescript",...c});m[a].$comment=p.substring(3);let h=n?o:l;e.existsSync(h)||e.mkdirSync(h,{recursive:!0}),await Promise.all([e.promises.writeFile(t.join(h,a+".ts"),u),e.promises.writeFile(t.join(h,a+".lokalise.json"),await r.format(g(m[a]).replace(/<(\/?)(\d+)>/g,"<$1tag-$2>"),{parser:"json",...c}))]),console.log(`Generated namespace ${a} in ${h}.`)},f=(e,t,a,r)=>{let n=(e=e.split(":")).pop(),s=1===e.length?e.pop():"translation",i=n.split("."),o=d[s];i.forEach((e,a)=>{o[e]?a===i.length-1&&o[e]!==t&&console.error(`Duplicate key: ${n}`):o[e]=a===i.length-1?t:{},o=o[e]}),r&&1!==r.length?r.forEach(e=>{m[s][n+`#${e}`]={translation:t[e],notes:a[e]}}):m[s][n]={translation:t,notes:a}},y=e=>{let t=new a.JSDOM(e).window.document,r=[],n=0,s=e=>{if(e.nodeType!==t.ELEMENT_NODE)return;e.childNodes.forEach(e=>{s(e)});let a=e.tagName.toLowerCase();if("body"===a)return;let i=0===e.attributes.length?void 0:Array.from(e.attributes).reduce((e,t)=>(e[t.name]=t.value,e),{});r.push({name:a,attributes:i});let o=t.createElement(`tag-${n}`);for(;e.firstChild;)o.appendChild(e.firstChild);e.parentNode.replaceChild(o,e),n++};s(t.body);let i=1,o=[],l=t.body.innerHTML.replace(/tag-/g,""),u=[];return l=l.replace(/<(\d+)>/g,(e,t)=>(u.push(r[t]),o.push({from:RegExp(`</${t}>`,"g"),to:`<*/${i}>`}),`<${i++}>`)),o.forEach(({from:e,to:t})=>{l=l.replace(e,t)}),{html:l=l.replace(/<\*\//g,"</"),elements:u}},v=e=>e.map((e,t)=>`### <${t+1}> ###
<${e.name} ${e.attributes?Object.keys(e.attributes).map(t=>`${t}="${e.attributes[t]}"`).join(" "):""} />
`).join(""),b=async t=>{let r=(e,t)=>{if("innerHTML"===t){let{html:t,elements:a}=y(e.innerHTML);return{value:t.trim(),desc:v(a)}}return{value:(t.startsWith("data-")?e.dataset[t.slice(5)]:e[t]||e.getAttribute(t)).trim(),desc:""}};f("translation:splash.loading","Loading LiveCodes\u2026","",["textContent"]),await Promise.all(t.map(async t=>{try{let n=(await e.promises.readFile(t,"utf8")).replace(/\s+/g," ").trim();new a.JSDOM(n).window.document.querySelectorAll("[data-i18n]").forEach(e=>{let t=e.getAttribute("data-i18n"),a=(e.getAttribute("data-i18n-prop")??"textContent").split(" "),{value:n,desc:s}=1===a.length?r(e,a[0]):a.reduce((t,a)=>{let n=r(e,a);return t.value[a]=n.value,t.desc[a]=n.desc,t},{value:{},desc:{}});f(t,n,s,a)})}catch(e){console.error(e)}}))},$=async t=>{await Promise.all(t.map(async t=>{try{let a=await e.promises.readFile(t,"utf8"),r=s.parse(a,{sourceType:"module",plugins:["typescript"]});n.traverse(r,{CallExpression(e){if("MemberExpression"===e.node.callee.type&&"Identifier"===e.node.callee.property.type&&"translateString"===e.node.callee.property.name&&e.node.arguments.length>=2&&"StringLiteral"===e.node.arguments[0].type&&"StringLiteral"===e.node.arguments[1].type){if(!e.node.arguments[2]||e.node.arguments[2].properties.every(e=>!e.key||!e.value||"isHTML"!==e.key.name||"isHTML"===e.key.name&&!0!==e.value.value))f(e.node.arguments[0].value,e.node.arguments[1].value,"",void 0);else{let{html:t,elements:a}=y(e.node.arguments[1].value);f(e.node.arguments[0].value,t.trim(),v(a),void 0)}}}})}catch(e){console.error(e)}}))},w=function(a,r=[]){return e.readdirSync(a).forEach(function(n){let s=a+t.sep+n;e.statSync(s).isDirectory()?r=w(s,r):r.push(s)}),r},T=async()=>{let e=process.argv.slice(2).filter(e=>!e.startsWith("-")),a=process.argv.includes("--save-temp"),r=[],n=[];e.length||e.push(...w(u)),r.push(...e.filter(e=>e.endsWith(".html")&&e.startsWith(t.resolve(u,`html${t.sep}`))).map(e=>t.resolve(u,e))),n.push(...e.filter(e=>e.endsWith(".ts")).map(e=>t.resolve(u,e))),await b(r),await $(n),h("translation",a),Object.keys(d["language-info"]).length>0&&h("language-info",a)};module.exports={generateTranslation:T,sortedJSONify:g,prettierConfig:c,autoGeneratedWarning:p},require.main===module&&T();

View File

@@ -0,0 +1,8 @@
import{LokaliseApi as e}from"@lokalise/node-api";import{execSync as o}from"child_process";import i from"fs";import r from"path";import a from"prettier";import{exit as s}from"process";import{autoGeneratedWarning as t,prettierConfig as n,sortedJSONify as l}from"./i18n-export.js";let p=r.resolve("src/livecodes/i18n/locales"),c=r.join(p,"tmp"),f=new e({apiKey:process.env.LOKALISE_API_TOKEN}),d=process.env.LOKALISE_PROJECT_ID,m=async(e,o)=>{let r=JSON.parse(await i.promises.readFile(e,"utf-8")),a={};for(let e in r){if(!o.has(e))continue;let i=e.split("."),s=i.pop(),t=a;i.forEach(e=>{t[e]||(t[e]={}),t=t[e]}),t[s]=r[e].replace(/tag-/g,"")}return a};(async()=>{let e="true"===process.env.CI,u=process.argv.slice(2).includes("--force"),g=process.argv.slice(2).includes("--local");e||u||(console.error("This script is intended to be run in CI mode or with --force flag."),s(1));let w=process.argv[2];w||(console.error("Branch name is required"),s(1));let $=r.resolve(process.env.LOKALISE_TEMP);if(!g){let e;console.log("Fetching translations from Lokalise...");let a=`${d}:${w}`,t=await f.files().async_download(a,{format:"json",original_filenames:!0,json_unescaped_slashes:!0,replace_breaks:!1,placeholder_format:"i18n"}),n=Date.now();for(;;){let o=await f.queuedProcesses().get(t.process_id,{project_id:a});if("finished"===o.status){e=o.details;break}Date.now()-n>6e4&&(console.error("Timeout exceeded. Aborting..."),s(1)),await new Promise(e=>setTimeout(e,2500))}console.log(`Downloading zip file from ${e.download_url}`);let l=r.join($,"locales.zip"),p=await fetch(e.download_url);await i.promises.writeFile(l,Buffer.from(await p.arrayBuffer())),console.log(`Extracting zip file to ${$}...`),o(`unzip -o ${l} -d ${$}`),await i.promises.unlink(l)}let _=await i.promises.readdir($);console.log(`Extracted languages to tmp directory, ${_.length} languages (including English) found.`),console.log("Checking if translation keys are outdated...");let h={},j={};for(let e of(o("npm run i18n-export -- --save-temp",{stdio:"pipe"}),(await i.promises.readdir(c)).filter(e=>e.endsWith(".lokalise.json")))){let o=e.split(".")[0],a=r.join(c,e),s=JSON.parse(await i.promises.readFile(a,"utf-8"));for(let e in h[o]={},s)h[o][e]=s[e].translation}let y=r.join($,"en");for(let e of(await i.promises.readdir(y))){let o=e.split(".")[0],a=r.join(y,e),s=JSON.parse(await i.promises.readFile(a,"utf-8"));for(let e in j[o]=new Set,s){if(h[o][e]){if(h[o][e]!==s[e]){console.warn(`Skipping: Key ${e} in namespace ${o} is outdated.`);continue}}else{console.warn(`Skipping: Key ${e} in namespace ${o} is missing in local translation.`);continue}j[o].add(e)}}for(let e of _){let o=r.join($,e);if(!(await i.promises.stat(o)).isDirectory()||"en"===e)continue;e=e.replace(/_/g,"-");let s=r.join(p,e);console.log(`Importing language ${e}...`),await i.promises.mkdir(s,{recursive:!0});let c=(await i.promises.readdir(o)).map(async e=>{let p=r.join(o,e),c=r.join(s,e.replace(".lokalise.json",".ts")),f=e.split(".")[0],d="translation"===f?"translation":"languageInfo",u="translation"===f?"I18nTranslation":"I18nLangInfoTranslation",g=l(await m(p,j[f])),w=`${t}
import type { ${u} } from '../models';
const ${d}: ${u} = ${g};
export default ${d};
`,$=await a.format(w,{parser:"typescript",...n});return i.promises.writeFile(c,$)});await Promise.all(c)}})();

View File

@@ -0,0 +1 @@
import e from"@babel/core";import r from"@babel/parser";import t from"fs";import s from"path";import{autoGeneratedWarning as o,sortedJSONify as l}from"./i18n-export.js";let a=(e,r="")=>Object.keys(e).reduce((t,s)=>{let o=e[s];return"object"==typeof o?{...t,...a(o,`${r}${s}.`)}:{...t,[`${r}${s}`]:o}},{}),n=e=>{if(!e)throw Error("Node is undefined or null");let r={};return e.properties.forEach(e=>{r[e.key.name||e.key.value]=i(e.value)}),r},i=e=>{switch(e.type){case"ObjectExpression":return n(e);case"ArrayExpression":return e.elements.map(i);case"StringLiteral":case"NumericLiteral":case"BooleanLiteral":return e.value;case"NullLiteral":return null;default:throw Error(`Unsupported node type: ${e.type}`)}},c=async i=>{let c=s.resolve("src/livecodes/i18n/locales/"+i);if(t.existsSync(c)){if("en"===i){console.warn("This script is not intended to be run for English language.\nPlease use `npm run i18n-export` instead.");return}}else{console.error(`Language ${c} does not exist.`);return}return Promise.all(t.readdirSync(c).filter(e=>e.endsWith(".ts")).map(e=>s.resolve(c,e)).map(async p=>{try{let u;console.log(`Generating Lokalise JSON for ${p} in language ${i}...`);let m=await t.promises.readFile(p,"utf8"),d=r.parse(m,{sourceType:"module",plugins:["typescript"]});e.traverse(d,{ObjectExpression(e){u=n(e.node),e.stop()}});let f={$comment:o.substring(3)};for(let[e,r]of Object.entries(a(u)))f[e]={translation:r};let y=s.resolve(c,p.replace(".ts",".lokalise.json"));await t.promises.writeFile(y,l(f).replace(/<(\/?)(\d+)>/g,"<$1tag-$2>"))}catch(e){console.error(e)}}))};(async()=>{let e=new Set(process.argv.slice(2));if(e.has("all")){e.delete("all");let r=s.resolve("src/livecodes/i18n/locales");t.readdirSync(r).filter(e=>t.statSync(s.resolve(r,e)).isDirectory()&&"en"!==e&&"tmp"!==e).forEach(r=>e.add(r))}await Promise.all([...e].map(c))})();

View File

@@ -0,0 +1,38 @@
name: i18n-update-notify
on:
pull_request_target:
branches: [develop]
types: [closed]
paths: ['src/livecodes/i18n/locales/**']
jobs:
notify:
name: Notify
runs-on: ubuntu-latest
if: github.event.pull_request.merged && github.event.sender.login != 'github-actions[bot]' && !startsWith(github.head_ref, 'i18n/')
steps:
- name: Generate Github Token for CI Bot
uses: actions/create-github-app-token@v1
id: generate-token
with:
app-id: ${{ secrets.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
- name: Create comment on PR
uses: actions/github-script@v6
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const commentBody = `## i18n Actions
Source PR has been merged into the default branch.
Maintainers can comment \`.i18n-update-push\` to trigger the i18n update workflow and push the changes to Lokalise.
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
})

View File

@@ -0,0 +1,235 @@
name: i18n-update-pull
on:
issue_comment:
types: [created]
env:
LOKALISE_PROJECT_ID: ${{ vars.LOKALISE_PROJECT_ID }}
LOKALISE_API_TOKEN: ${{ secrets.LOKALISE_API_TOKEN }}
NODE_VERSION: 18.x
CI: true
jobs:
precheck:
name: Pre-check
runs-on: ubuntu-latest
if: github.event.issue.pull_request && github.event.issue.pull_request.merged_at && github.event.issue.state == 'closed' && github.event.comment.body == '.i18n-update-pull' && (github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
outputs:
skip: ${{ steps.fetch-pr.outputs.skip }}
skipReason: ${{ steps.fetch-pr.outputs.skipReason }}
newBranch: ${{ steps.fetch-pr.outputs.newBranch }}
branch: ${{ steps.fetch-pr.outputs.branch }}
steps:
- name: Generate Github Token for CI Bot
uses: actions/create-github-app-token@v1
id: generate-token
with:
app-id: ${{ secrets.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
- name: Check out repository
uses: actions/checkout@v4
with:
token: ${{ steps.generate-token.outputs.token }}
- name: Fetch PR details
id: fetch-pr
run: |
PR_DETAILS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "${{ github.event.issue.pull_request.url }}")
PR_BRANCH=$(echo "$PR_DETAILS" | jq -r '.head.ref')
skip () {
echo "$1 Exiting..."
echo "skip=true" >> $GITHUB_OUTPUT
echo "skipReason=$1" >> $GITHUB_OUTPUT
}
if [[ $PR_BRANCH == "i18n/"* ]]; then
skip "Branch \`$PR_BRANCH\` is a i18n branch."
fi
PR_BRANCH=$(echo "$PR_DETAILS" | jq -r '.head.label' | sed 's/:/\//g')
NEW_BRANCH="i18n/$PR_BRANCH"
echo "newBranch=$NEW_BRANCH" >> $GITHUB_OUTPUT
echo "branch=$PR_BRANCH" >> $GITHUB_OUTPUT
git config --global user.name "livecodes-ci[bot]"
git config --global user.email "186997172+livecodes-ci[bot]@users.noreply.github.com"
if [[ ! $(git ls-remote --heads origin $NEW_BRANCH) ]]; then
skip "Branch \`$NEW_BRANCH\` does not exist."
fi
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
runner:
name: Runner
runs-on: ubuntu-latest
needs: precheck
if: needs.precheck.outputs.skip != 'true'
env:
NEW_BRANCH: ${{ needs.precheck.outputs.newBranch }}
PR_BRANCH: ${{ needs.precheck.outputs.branch }}
steps:
- name: Generate Github Token for CI Bot
uses: actions/create-github-app-token@v1
id: generate-token
with:
app-id: ${{ secrets.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ needs.precheck.outputs.newBranch }}
token: ${{ steps.generate-token.outputs.token }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm ci
- name: Import from Lokalise
run: |
mkdir -p $LOKALISE_TEMP && touch $LOKALISE_TEMP/locales.zip && npm run i18n-update-pull -- $PR_BRANCH && rm -rf $LOKALISE_TEMP
env:
LOKALISE_TEMP: lokalise_tmp
- name: Generate Lokalise JSON files
run: npm run i18n-lokalise-json all
- name: Linting and fixing
run: npm run fix
- name: Commit changes
run: |
git config --global user.name "livecodes-ci[bot]"
git config --global user.email "186997172+livecodes-ci[bot]@users.noreply.github.com"
git add .
# Only commit if there are changes
git diff-index --quiet HEAD || git commit -m "i18n: pull translation from Lokalise"
# Save SHA of the latest commit to locale
echo "LAST_COMMIT_SHA=$(git log -n 1 --format="%H" -- src/livecodes/i18n/locales)" >> $GITHUB_ENV
- name: Push changes
run: git push origin $NEW_BRANCH
- name: Create a new i18n PR, comment on source PR and reaction
uses: actions/github-script@v7
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const repoURL = context.payload.repository.html_url;
const branchURL = `${repoURL}/tree/${process.env.NEW_BRANCH}`;
const prTitle = `i18n: ${{ github.event.issue.title }}`;
const prBody = `## What type of PR is this? (check all applicable)
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 📝 Documentation Update
- [ ] 🎨 Style
- [ ] ♻️ Code Refactor
- [ ] 🔥 Performance Improvements
- [ ] ✅ Test
- [ ] 🤖 Build
- [ ] 🔁 CI
- [ ] 📦 Chore (Release)
- [ ] ⏩ Revert
- [x] 🌐 Internationalization / Translation
## Description
### i18n Actions: \`.i18n-update-pull\`
Localization pulled from Lokalise.
| Name | Description |
| --- | --- |
| **i18n Branch** | [\`${process.env.NEW_BRANCH}\`](${branchURL}) |
| **Last Commit SHA** | ${process.env.LAST_COMMIT_SHA} |
## Related Tickets & Documents
- **Source PR**: #${{ github.event.issue.number }}
`;
const prInfo = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: prTitle,
body: prBody,
head: process.env.NEW_BRANCH,
base: '${{ github.event.repository.default_branch }}'
});
const commentBody = `## i18n Actions: \`.i18n-update-pull\`
Localization pulled from Lokalise.
| Name | Description |
| --- | --- |
| **i18n Branch** | [\`${process.env.NEW_BRANCH}\`](${branchURL}) |
| **Last Commit SHA** | ${process.env.LAST_COMMIT_SHA} |
| **i18n PR** | #${prInfo.data.number} |
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
})
github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ github.event.comment.id }},
content: 'rocket'
});
exception:
name: Exception
runs-on: ubuntu-latest
needs: precheck
if: needs.precheck.outputs.skip == 'true'
env:
SKIP_REASON: ${{ needs.precheck.outputs.skipReason }}
steps:
- name: Generate Github Token for CI Bot
uses: actions/create-github-app-token@v1
id: generate-token
with:
app-id: ${{ secrets.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
- name: Create reaction on PR
uses: actions/github-script@v7
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const runURL = `${context.payload.repository.html_url}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const commentBody = `## i18n Actions: \`.i18n-update-pull\`
Failed to perform action due to following reason: **${process.env.SKIP_REASON}**
Please check [action logs](${runURL}) for more details.
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
})
github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ github.event.comment.id }},
content: 'confused'
});

View File

@@ -0,0 +1,195 @@
name: i18n-update-push
on:
issue_comment:
types: [created]
env:
LOKALISE_PROJECT_ID: ${{ vars.LOKALISE_PROJECT_ID }}
LOKALISE_API_TOKEN: ${{ secrets.LOKALISE_API_TOKEN }}
NODE_VERSION: 18.x
CI: true
jobs:
precheck:
name: Pre-check
runs-on: ubuntu-latest
if: github.event.issue.pull_request && github.event.issue.pull_request.merged_at && github.event.issue.state == 'closed' && github.event.comment.body == '.i18n-update-push' && (github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
outputs:
skip: ${{ steps.fetch-pr.outputs.skip }}
skipReason: ${{ steps.fetch-pr.outputs.skipReason }}
newBranch: ${{ steps.fetch-pr.outputs.newBranch }}
branch: ${{ steps.fetch-pr.outputs.branch }}
steps:
- name: Generate Github Token for CI Bot
uses: actions/create-github-app-token@v1
id: generate-token
with:
app-id: ${{ secrets.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
- name: Check out repository
uses: actions/checkout@v4
with:
token: ${{ steps.generate-token.outputs.token }}
- name: Fetch PR details
id: fetch-pr
run: |
PR_DETAILS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "${{ github.event.issue.pull_request.url }}")
PR_BRANCH=$(echo "$PR_DETAILS" | jq -r '.head.ref')
if [[ $PR_BRANCH == "i18n/"* ]]; then
SKIP_REASON="Branch \`$PR_BRANCH\` is a i18n branch."
echo "$SKIP_REASON Exiting..."
echo "skip=true" >> $GITHUB_OUTPUT
echo "skipReason=$SKIP_REASON" >> $GITHUB_OUTPUT
fi
# Use branch name prefixed with owner name
PR_BRANCH=$(echo "$PR_DETAILS" | jq -r '.head.label' | sed 's/:/\//g')
NEW_BRANCH="i18n/$PR_BRANCH"
echo "newBranch=$NEW_BRANCH" >> $GITHUB_OUTPUT
echo "branch=$PR_BRANCH" >> $GITHUB_OUTPUT
git config --global user.name "livecodes-ci[bot]"
git config --global user.email "186997172+livecodes-ci[bot]@users.noreply.github.com"
if [[ $(git ls-remote --heads origin $NEW_BRANCH) ]]; then
SKIP_REASON="Branch \`$NEW_BRANCH\` already exists."
echo "$SKIP_REASON Exiting..."
echo "skip=true" >> $GITHUB_OUTPUT
echo "skipReason=$SKIP_REASON" >> $GITHUB_OUTPUT
fi
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
runner:
name: Runner
runs-on: ubuntu-latest
needs: precheck
if: needs.precheck.outputs.skip != 'true'
env:
NEW_BRANCH: ${{ needs.precheck.outputs.newBranch }}
PR_BRANCH: ${{ needs.precheck.outputs.branch }}
steps:
- name: Generate Github Token for CI Bot
uses: actions/create-github-app-token@v1
id: generate-token
with:
app-id: ${{ secrets.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
- name: Check out repository
uses: actions/checkout@v4
with:
token: ${{ steps.generate-token.outputs.token }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm ci
- name: Create new branch
run: git checkout -b $NEW_BRANCH
- name: Linting and fixing
run: npm run fix
- name: Commit changes
run: |
git config --global user.name "livecodes-ci[bot]"
git config --global user.email "186997172+livecodes-ci[bot]@users.noreply.github.com"
git add .
# Only commit if there are changes
git diff-index --quiet HEAD || git commit -m "i18n: update source texts"
# Save SHA of the latest commit to English locale
echo "LAST_COMMIT_SHA=$(git log -n 1 --format="%H" -- src/livecodes/i18n/locales/en)" >> $GITHUB_ENV
- name: Push changes
run: git push origin $NEW_BRANCH
- name: Push source texts to Lokalise
run: npm run i18n-update-push -- $PR_BRANCH
- name: Create comment and reaction on PR
uses: actions/github-script@v7
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const repoURL = context.payload.repository.html_url;
const branchURL = `${repoURL}/tree/${process.env.NEW_BRANCH}`;
const commentBody = `## i18n Actions: \`.i18n-update-push\`
Localization updated and pushed to [Lokalise](https://app.lokalise.com/project/${process.env.LOKALISE_PROJECT_ID}/?branch=${process.env.PR_BRANCH}).
| Name | Description |
| --- | --- |
| **New Branch for i18n** | [\`${process.env.NEW_BRANCH}\`](${branchURL}) |
| **Last Commit SHA** | ${process.env.LAST_COMMIT_SHA} |
Maintainers can comment \`.i18n-update-pull\` after translation is done to trigger the i18n pull workflow and pull the changes back to Github.
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
})
github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ github.event.comment.id }},
content: 'rocket'
});
exception:
name: Exception
runs-on: ubuntu-latest
needs: precheck
if: needs.precheck.outputs.skip == 'true'
env:
SKIP_REASON: ${{ needs.precheck.outputs.skipReason }}
steps:
- name: Generate Github Token for CI Bot
uses: actions/create-github-app-token@v1
id: generate-token
with:
app-id: ${{ secrets.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
- name: Create comment and reaction on PR
uses: actions/github-script@v7
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const runURL = `${context.payload.repository.html_url}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const commentBody = `## i18n Actions: \`.i18n-update-push\`
Failed to perform action due to following reason: **${process.env.SKIP_REASON}**
Please check [action logs](${runURL}) for more details.
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
})
github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ github.event.comment.id }},
content: 'confused'
});

View File

@@ -0,0 +1,179 @@
name: i18n-update-scheduled
# Triggered weekly to update source texts and push them to Lokalise, then pull the translations back to Github.
# Work on i18n/develop branch.
on:
schedule:
- cron: '0 0 * * 0'
push:
branches:
- develop
workflow_dispatch:
env:
LOKALISE_PROJECT_ID: ${{ vars.LOKALISE_PROJECT_ID }}
LOKALISE_API_TOKEN: ${{ secrets.LOKALISE_API_TOKEN }}
BRANCH: i18n/develop
LOKALISE_BRANCH: master
NODE_VERSION: 18.x
CI: true
jobs:
update:
name: Push and Pull
runs-on: ubuntu-latest
steps:
- name: Generate Github Token for CI Bot
uses: actions/create-github-app-token@v1
id: generate-token
with:
app-id: ${{ secrets.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate-token.outputs.token }}
- name: Switch to i18n branch
run: |
git config --global user.name "livecodes-ci[bot]"
git config --global user.email "186997172+livecodes-ci[bot]@users.noreply.github.com"
if [[ $(git ls-remote --heads origin $BRANCH) ]]; then
git config pull.rebase false
git fetch origin $BRANCH:$BRANCH
git checkout $BRANCH
else
git checkout -b $BRANCH
fi
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm ci
# - name: Linting and fixing
# run: npm run fix
# - name: Commit changes
# run: |
# git add .
# # Only commit if there are changes
# git diff-index --quiet HEAD || git commit -m "i18n: update source texts"
# # Save SHA of the latest commit to English locale
# echo "LAST_COMMIT_SHA_PUSH=$(git log -n 1 --format="%H" -- src/livecodes/i18n/locales/en)" >> $GITHUB_ENV
# - name: Push changes
# run: git push origin $BRANCH
# - name: Push source texts to Lokalise
# run: npm run i18n-update-push -- $LOKALISE_BRANCH
- name: Import from Lokalise
run: |
mkdir -p $LOKALISE_TEMP && touch $LOKALISE_TEMP/locales.zip && npm run i18n-update-pull -- $LOKALISE_BRANCH && rm -rf $LOKALISE_TEMP
env:
LOKALISE_TEMP: lokalise_tmp
- name: Linting and fixing
run: npm run fix
- name: Commit changes
run: |
git add .
# Only commit if there are changes
git diff-index --quiet HEAD || git commit -m "i18n: pull translation from Lokalise"
# Save SHA of the latest commit to locale
echo "LAST_COMMIT_SHA_PULL=$(git log -n 1 --format="%H" -- src/livecodes/i18n/locales)" >> $GITHUB_ENV
- name: Push changes
run: |
git pull origin ${{ github.event.repository.default_branch }} || {
echo "Failed to pull from ${{ github.event.repository.default_branch }}."
echo "Please manually pull the changes, solve potential conflicts, and re-run the workflow."
echo "::error title=Pull failed::Failed to pull from ${{ github.event.repository.default_branch }}."
exit 1
}
git push origin $BRANCH
- name: Check if has differences between ${{ env.BRANCH }} and ${{ github.event.repository.default_branch }}
id: check-diff
run: |
DIFF=$(git diff --name-only $BRANCH origin/${{ github.event.repository.default_branch }})
if [[ -z $DIFF ]]; then
echo "No difference between $BRANCH and ${{ github.event.repository.default_branch }}."
echo "SKIP=true" >> $GITHUB_OUTPUT
fi
echo "LAST_COMMIT_SHA_PUSH=$(git log -n 1 --format="%H" -- src/livecodes/i18n/locales/en)" >> $GITHUB_ENV
- name: Create a new i18n PR if not exists
uses: actions/github-script@v7
if: steps.check-diff.outputs.SKIP != 'true'
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const prInfo = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: process.env.BRANCH
});
if (prInfo.data.length > 0) {
return;
}
console.log(`Creating a new i18n PR from ${process.env.BRANCH}...`);
const repoURL = context.payload.repository.html_url;
const branchURL = `${repoURL}/tree/${process.env.BRANCH}`;
const prTitle = `i18n: scheduled update from ${process.env.BRANCH}`;
const prBody = `## What type of PR is this? (check all applicable)
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 📝 Documentation Update
- [ ] 🎨 Style
- [ ] ♻️ Code Refactor
- [ ] 🔥 Performance Improvements
- [ ] ✅ Test
- [ ] 🤖 Build
- [ ] 🔁 CI
- [ ] 📦 Chore (Release)
- [ ] ⏩ Revert
- [x] 🌐 Internationalization / Translation
## Description
### i18n Actions: \`.i18n-update-scheduled\`
Scheduled update of source texts and translations.
| Name | Description |
| --- | --- |
| **Last Commit SHA (Push)** | ${process.env.LAST_COMMIT_SHA_PUSH} |
| **Last Commit SHA (Pull)** | ${process.env.LAST_COMMIT_SHA_PULL} |
`;
github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: prTitle,
body: prBody,
head: process.env.BRANCH,
base: '${{ github.event.repository.default_branch }}'
});

View File

@@ -0,0 +1,2 @@
import{LokaliseApi as e}from"@lokalise/node-api";import o from"fs";import r from"path";import{exit as s}from"process";let i=r.resolve("src/livecodes/i18n/locales/en"),a=new e({apiKey:process.env.LOKALISE_API_TOKEN}),n=process.env.LOKALISE_PROJECT_ID,t={cleanup_mode:!0,replace_modified:!0,convert_placeholders:!1};(()=>{let e="true"===process.env.CI,l=process.argv.slice(2).includes("--force");e||l||(console.error("This script is intended to be run in CI mode or with --force flag."),s(1));let c=process.argv[2];c||(console.error("Branch name is required"),s(1)),o.existsSync(i)||(console.error(`Directory ${i} doesn't exist, please run i18n-export first`),s(1)),o.readdir(i,async(e,l)=>{e&&(console.error(e),s(1));let p=l.filter(e=>e.endsWith(".lokalise.json")).map(e=>({data:o.readFileSync(r.join(i,e)).toString("base64"),filename:e,lang_iso:"en"}));console.log(`Following files will be uploaded to Lokalise:
${p.map(e=>e.filename).join("\n")}`),(await a.branches().list({project_id:n})).items.some(e=>e.name===c)||(console.log(`Branch ${c} doesn't exist. Creating...`),await a.branches().create({name:c},{project_id:n}));let d=(await Promise.all(p.map(e=>a.files().upload(`${n}:${c}`,{...e,...t})))).map(e=>e.process_id);console.log("Waiting for files to be processed...");let m=Date.now();for(;!(await Promise.all(d.map(e=>a.queuedProcesses().get(e,{project_id:`${n}:${c}`})))).every(e=>"finished"===e.status);)Date.now()-m>6e4&&(console.error("Timeout exceeded. Aborting..."),s(1)),await new Promise(e=>setTimeout(e,2500))})})();

View File

@@ -0,0 +1 @@
const e=require("fs"),t=require("path"),a={version:1.1,globalAttributes:[{name:"data-i18n",description:"The key of the translation for current element."},{name:"data-i18n-prop",description:"Attributes of the element that should be translated, separated by space.",valueSet:"i18nProps"},{name:"data-hint",description:"The tooltip of the element."}],valueSets:[]},l=async()=>{await new Promise((t,l)=>{e.readFile("src/livecodes/i18n/locales/models.ts","utf8",(e,s)=>{if(e)console.error(e),l(e);else{let e=s.match(/I18nAttributes.+?{([\s\S]*?)}/)[1].split("\n").map(e=>e.trim().replace(/['|;?]/g,"").split(":")[0]).filter(e=>""!==e);a.valueSets.push({name:a.globalAttributes[1].valueSet,values:e.map(e=>({name:e}))}),t()}})});let l=t.resolve(__dirname,"../.vscode/html.html-data.json");e.writeFileSync(l,JSON.stringify(a,null,2)),console.log(`HTML Intellisense schema generated at ${l}`)};module.exports={generateHTMLIntellisense:l},require.main===module&&l();