diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 91e6f84ab8..e4da767415 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,11 +5,6 @@ on: push: branches: [master] -permissions: - contents: read - pages: write - id-token: write - concurrency: group: "deploy" cancel-in-progress: true @@ -17,11 +12,18 @@ concurrency: jobs: build: runs-on: ubuntu-latest + + # If you are forking and want to set up your own website, adjust the repository and branch + # below to match your repository or remove the condition entirely. + if: ${{ github.repository == 'TurboWarp/extensions' && github.ref == 'refs/heads/master' }} + steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af with: node-version: 20.x - name: Install dependencies @@ -29,7 +31,7 @@ jobs: - name: Build for production run: npm run build - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa with: path: ./build/ @@ -37,9 +39,12 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write + id-token: write runs-on: ubuntu-latest needs: build steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e diff --git a/.github/workflows/download-translations.yml b/.github/workflows/download-translations.yml new file mode 100644 index 0000000000..5693ed7751 --- /dev/null +++ b/.github/workflows/download-translations.yml @@ -0,0 +1,93 @@ +name: Download translations + +on: + workflow_dispatch: + +concurrency: + group: "download-translations" + cancel-in-progress: true + +jobs: + download-translations: + runs-on: ubuntu-latest + + # This workflow is not useful to forks without setting up Transifex and modifying the + # workflow to use your organization, project, resources, API token, ... + if: ${{ github.repository == 'TurboWarp/extensions' && github.ref == 'refs/heads/master' }} + + steps: + - name: Checkout fork + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + # Commits will be written to this fork, then pull requested to the main repository. + repository: "DangoCat/extensions" + token: "${{ secrets.UPDATE_TRANSLATIONS_FORK_GH_TOKEN }}" + # We will push later so the token has to be stored. + persist-credentials: true + - name: Checkout upstream + run: | + git remote add upstream "https://github.com/$UPSTREAM_REPO.git" + git fetch upstream + git checkout upstream/master + env: + UPSTREAM_REPO: "TurboWarp/extensions" + - name: Install Node.js + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af + with: + node-version: 20.x + - name: Install dependencies + run: npm ci + - name: Download translations + run: npm run download-translations + env: + TRANSIFEX_TOKEN: "${{ secrets.TRANSIFEX_TOKEN }}" + TRANSIFEX_ORGANIZATION: "turbowarp" + TRANSIFEX_PROJECT: "turbowarp" + TRANSIFEX_RUNTIME_RESOURCE: "extensions" + TRANSIFEX_METADATA_RESOURCE: "extension-metadata" + - name: Delete old branches, commit, push, pull request + run: | + if [[ ! $(git status --porcelain) ]]; then + echo "No changes" + exit 0 + fi + + # Remove old branches, which also closes the pull requests + all_branches=$(GH_TOKEN="$FORK_GH_TOKEN" gh api "repos/$FORK_REPO/branches" --paginate | jq -r '.[].name') + for branch in $all_branches; do + if [[ $branch == update-translations-* ]]; then + echo "Deleting branch: $branch" + git push -d origin "$branch" + else + echo "Keeping branch: $branch" + fi + done + + # Create new branch + new_branch="update-translations-$(date -u +%Y%m%d%H%M%S)" + git checkout -b "$new_branch" + + # Commit + date="$(date -u "+%Y-%m-%d")" + title="[Automated] Update translations $date" + git add . + git config --global user.name "DangoCat[bot]" + git config --global user.email "dangocat@users.noreply.github.com" + git commit -m "$title" + + # Push + git push origin "$new_branch" + + # Create pull request + GH_TOKEN="$UPSTREAM_GH_TOKEN" gh pr create \ + --head "dangocat:$new_branch" \ + --repo "$UPSTREAM_REPO" \ + --title "$title" \ + --body "This pull request was made by a robot." + env: + FORK_REPO: "DangoCat/extensions" + # This token has contents write permissions on fork repository + FORK_GH_TOKEN: "${{ secrets.UPDATE_TRANSLATIONS_FORK_GH_TOKEN }}" + UPSTREAM_REPO: "${{ github.repository }}" + # This token has pull request write permissions on upstream repository + UPSTREAM_GH_TOKEN: "${{ secrets.UPDATE_TRANSLATIONS_UPSTREAM_GH_TOKEN }}" diff --git a/.github/workflows/format-pr.yml b/.github/workflows/format-pr.yml new file mode 100644 index 0000000000..d65d1e39d7 --- /dev/null +++ b/.github/workflows/format-pr.yml @@ -0,0 +1,127 @@ +name: Format pull request + +on: + workflow_dispatch: + issue_comment: + types: [created] + +permissions: {} + +jobs: + # Handling workflow_dispatch is simple. Just checkout whatever branch it was run on. + # The workflow will run in that repository's context and thus can safely get write permissions. + manual-dispatch: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + # Commits made by workflow_dispatch trigger can trigger new workflows to run, + # so just use the default workflow token. + # Credentials needed for pushing changes at the end. + # This is already the default, but it's good to be explicit about this. + persist-credentials: true + - name: Install Node.js + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af + with: + node-version: 20.x + - name: Install dependencies + run: npm ci + - name: Format + run: npm run format + - name: Commit + run: | + git config --global user.name "$GITHUB_ACTOR" + git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" + git stage . + git commit --author "DangoCat[bot] " -m "[Automated] Format code" || echo "No changes to commit" + - name: Push + run: git push + + # Comments are more complicated because the action runs in the context of TurboWarp/extensions but + # we are processing content from the possibly malicious pull request. We break this into two + # separate jobs. + # The first job downloads the pull request, formats it, and uploads the new files to an artifact. + # Important to have no permissions for this because the code can't be trusted. + comment-format-untrusted: + runs-on: ubuntu-latest + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '!format') && + ( + github.event.comment.author_association == 'MEMBER' || + github.event.comment.user.id == github.event.issue.user.id + ) + steps: + - name: Checkout upstream + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + repository: TurboWarp/extensions + persist-credentials: false + - name: Checkout pull request + run: gh pr checkout "$PR_NUM" + env: + PR_NUM: "${{ github.event.issue.number }}" + GH_TOKEN: "${{ github.token }}" + - name: Install Node.js + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af + with: + node-version: 20.x + - name: Install dependencies + run: npm ci + - name: Format + run: npm run format + - name: Upload formatted code + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b + with: + name: comment-format-untrusted-artifact + path: extensions/ + if-no-files-found: error + retention-days: 7 + + # Second job downloads the artifact, extracts it, and pushes it. + comment-push: + runs-on: ubuntu-latest + needs: comment-format-untrusted + steps: + - name: Checkout upstream + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + repository: TurboWarp/extensions + # Can't use the default workflow token because commits made by it won't cause more + # workflows to un, so any commits it pushes get stuck in limbo waiting for workflows + # to run that will never run. + # Can't use a deploy key because it won't be able to access the fork that the pull + # request is coming from. + # Thus we use a manually created fine-grained personal access token under the + # @DangoCat account. + token: "${{ secrets.FORMAT_PR_GH_TOKEN }}" + # Credentials needed for pushing changes at the end. + # This is already the default, but it's good to be explicit about this. + persist-credentials: true + - name: Checkout pull request + run: gh pr checkout "$PR_NUM" + env: + PR_NUM: "${{ github.event.issue.number }}" + GH_TOKEN: "${{ github.token }}" + - name: Download formatted code + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 + with: + name: comment-format-untrusted-artifact + path: extensions + - name: Commit + run: | + git config --global user.name "$GITHUB_ACTOR" + git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" + git stage . + git commit --author "DangoCat[bot] " -m "[Automated] Format code" || echo "No changes to commit" + - name: Push + # Explicitly set push.default to upstream, otherwise by default git might complain about us being on a + # branch called "DangoCat/master" but the corresponding branch on remote "DangoCat" is just "master". + run: | + git config --global push.default upstream + git push diff --git a/.github/workflows/upload-translations.yml b/.github/workflows/upload-translations.yml new file mode 100644 index 0000000000..8703a351e8 --- /dev/null +++ b/.github/workflows/upload-translations.yml @@ -0,0 +1,36 @@ +name: Upload translations + +on: + workflow_dispatch: + +concurrency: + group: "upload-translations" + cancel-in-progress: true + +jobs: + upload-translations: + runs-on: ubuntu-latest + + # This workflow is not useful to forks without setting up Transifex and modifying the + # workflow to use your organization, project, resources, API token, ... + if: ${{ github.repository == 'TurboWarp/extensions' && github.ref == 'refs/heads/master' }} + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false + - name: Install Node.js + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af + with: + node-version: 20.x + - name: Install dependencies + run: npm ci + - name: Upload translations + run: npm run upload-translations + env: + TRANSIFEX_TOKEN: "${{ secrets.TRANSIFEX_TOKEN }}" + TRANSIFEX_ORGANIZATION: "turbowarp" + TRANSIFEX_PROJECT: "turbowarp" + TRANSIFEX_RUNTIME_RESOURCE: "extensions" + TRANSIFEX_METADATA_RESOURCE: "extension-metadata" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c02a81ea48..4132701ee6 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,9 +8,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af with: node-version: 20.x cache: npm @@ -23,9 +25,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af with: node-version: 20.x cache: npm @@ -38,9 +42,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af with: node-version: 20.x cache: npm diff --git a/.gitignore b/.gitignore index b6b3ebdff6..365b37bba8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build-l10n # Various operating system caches thumbs.db +desktop.ini .DS_Store # Popular editors diff --git a/development/builder.js b/development/builder.js index 0a91fab6b5..b4024675a7 100644 --- a/development/builder.js +++ b/development/builder.js @@ -787,37 +787,34 @@ class Builder { build.files[`/${filename}`] = new BuildFile(absolutePath); } - if (this.mode !== "desktop") { - for (const [filename, absolutePath] of recursiveReadDirectory( - this.docsRoot - )) { - if (!filename.endsWith(".md")) { - continue; - } - const extensionSlug = filename.split(".")[0]; - const file = new DocsFile(absolutePath, extensionSlug); - extensionsWithDocs.add(extensionSlug); - build.files[`/${extensionSlug}.html`] = file; + for (const [filename, absolutePath] of recursiveReadDirectory( + this.docsRoot + )) { + if (!filename.endsWith(".md")) { + continue; } + const extensionSlug = filename.split(".")[0]; + const file = new DocsFile(absolutePath, extensionSlug); + extensionsWithDocs.add(extensionSlug); + build.files[`/${extensionSlug}.html`] = file; + } - const scratchblocksPath = pathUtil.join( - __dirname, - "../node_modules/@turbowarp/scratchblocks/build/scratchblocks.min.js" - ); - build.files["/docs-internal/scratchblocks.js"] = new BuildFile( - scratchblocksPath - ); + // Don't rely on node_modules being stored in a specific location or having a specific structure + // so that this works when we are a dependency in a bigger npm tree. + const scratchblocksPath = require.resolve("@turbowarp/scratchblocks"); + build.files["/docs-internal/scratchblocks.js"] = new BuildFile( + scratchblocksPath + ); - build.files["/index.html"] = new HomepageFile( - extensionFiles, - extensionImages, - featuredExtensionSlugs, - extensionsWithDocs, - samples, - this.mode - ); - build.files["/sitemap.xml"] = new SitemapFile(build); - } + build.files["/index.html"] = new HomepageFile( + extensionFiles, + extensionImages, + featuredExtensionSlugs, + extensionsWithDocs, + samples, + this.mode + ); + build.files["/sitemap.xml"] = new SitemapFile(build); build.files["/generated-metadata/extensions-v0.json"] = new JSONMetadataFile( diff --git a/development/docs-template.ejs b/development/docs-template.ejs index f3915ad923..d704b5d46f 100644 --- a/development/docs-template.ejs +++ b/development/docs-template.ejs @@ -46,6 +46,7 @@ display: flex; max-width: 600px; margin: 0 auto; + justify-content: space-between; } nav a { display: flex; @@ -95,6 +96,18 @@ } } + .view-in-browser { + display: none; + } + .is-desktop .view-in-browser { + display: flex; + } + .is-desktop nav > div, .is-desktop .container { + /* Desktop app shows this page in a window, not a browser tab, so don't need to */ + /* restrict the width of the content for readability. */ + max-width: 100%; + } + code { background-color: #eee; border-radius: 4px; @@ -133,10 +146,15 @@ diff --git a/development/download-translations.js b/development/download-translations.js new file mode 100644 index 0000000000..cbaa2021cb --- /dev/null +++ b/development/download-translations.js @@ -0,0 +1,240 @@ +const pathUtil = require("path"); +const fs = require("fs"); +const { + transifexApi, + ORGANIZATION_NAME, + PROJECT_NAME, + RUNTIME_RESOURCE, + METADATA_RESOURCE, + SOURCE_LOCALE, +} = require("./transifex-common"); + +/** + * @template T + * @param {T} obj + * @returns {T} obj but keys are sorted + */ +const withSortedKeyOrder = (obj) => { + const result = {}; + for (const key of Object.keys(obj).sort()) { + const value = obj[key]; + if (typeof value === "object") { + result[key] = withSortedKeyOrder(obj); + } else { + result[key] = value; + } + } + return result; +}; + +/** + * @param {string} resource + * @returns {Promise>} + */ +const getResourceStatistics = async (resource) => { + const iterator = transifexApi.resource_language_stats + .filter({ + project: `o:${ORGANIZATION_NAME}:p:${PROJECT_NAME}`, + resource: `o:${ORGANIZATION_NAME}:p:${PROJECT_NAME}:r:${resource}`, + }) + .all(); + const locales = {}; + for await (const languageData of iterator) { + const localeCode = languageData.id.match(/\bl:([\w\d-]+)/)[1]; + const translatedStrings = languageData.attributes.translated_strings; + locales[localeCode] = translatedStrings; + } + return withSortedKeyOrder(locales); +}; + +/** + * @param {object} strings JSON with { string: string, developer_comment: string } values. + * @returns {object} JSON with string values. + */ +const removeDeveloperComments = (strings) => { + const result = {}; + for (const [key, value] of Object.entries(strings)) { + if (typeof value.string === "string") { + result[key] = value.string; + } else { + result[key] = removeDeveloperComments(value); + } + } + return result; +}; + +/** + * @param {object} strings + * @returns {object} + */ +const removeEmptyTranslations = (strings) => { + const result = {}; + for (const [key, value] of Object.entries(strings)) { + if (typeof value === "object") { + result[key] = removeEmptyTranslations(value); + } else if (value !== "") { + result[key] = value; + } + } + return result; +}; + +/** + * @param {object} source + * @param {object} translated + * @returns {object} + */ +const removeUnchangedTranslations = (source, translated) => { + const result = {}; + for (const [key, translatedValue] of Object.entries(translated)) { + const sourceValue = source[key]; + if (typeof translatedValue === "object") { + const recursiveResult = removeUnchangedTranslations( + sourceValue, + translatedValue + ); + if (Object.keys(recursiveResult).length > 0) { + result[key] = recursiveResult; + } + } else if (translatedValue !== sourceValue) { + result[key] = translatedValue; + } + } + return result; +}; + +/** + * @param {number} ms + * @returns {Promise} + */ +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * @param {string} url + * @returns {Promise} JSON response + */ +const persistentFetch = async (url) => { + for (let i = 0; i < 5; i++) { + try { + const res = await fetch(url); + const json = await res.json(); + return json; + } catch (e) { + const sleepFor = Math.random() * 2 ** i * 1000; + await sleep(sleepFor); + } + } +}; + +/** + * @param {string} resource + * @param {string} locale + * @returns {Promise} + */ +const downloadTranslatedResource = async (resource, locale) => { + console.log(`Starting download request for ${resource} ${locale}`); + + let urlToDownload; + if (locale === SOURCE_LOCALE) { + urlToDownload = await transifexApi.ResourceStringsAsyncDownload.download({ + resource: { + data: { + id: `o:${ORGANIZATION_NAME}:p:${PROJECT_NAME}:r:${resource}`, + type: "resources", + }, + }, + }); + } else { + urlToDownload = + await transifexApi.ResourceTranslationsAsyncDownload.download({ + mode: "onlytranslated", + resource: { + data: { + id: `o:${ORGANIZATION_NAME}:p:${PROJECT_NAME}:r:${resource}`, + type: "resources", + }, + }, + language: { + data: { + id: `l:${locale}`, + type: "languages", + }, + }, + }); + } + + console.log(`Started download request for ${resource} ${locale}`); + const rawTranslations = await persistentFetch(urlToDownload); + + console.log(`Downloaded data for ${resource} ${locale}`); + const withoutDeveloperComments = removeDeveloperComments(rawTranslations); + const withoutEmptyTranslations = removeEmptyTranslations( + withoutDeveloperComments + ); + const sortedTranslations = withSortedKeyOrder(withoutEmptyTranslations); + return sortedTranslations; +}; + +/** + * @param {string} resource + * @returns {Promise} + */ +const downloadAllResourceTranslations = async (resource) => { + const transifexStatistics = await getResourceStatistics(resource); + const localesToFetch = []; + for (const [locale, translatedStringCount] of Object.entries( + transifexStatistics + )) { + if (translatedStringCount > 0) { + localesToFetch.push(locale); + } + } + + const entries = await Promise.all( + localesToFetch.map(async (locale) => { + const translatedStrings = await downloadTranslatedResource( + resource, + locale + ); + return [locale, translatedStrings]; + }) + ); + + const sourceStrings = entries.find((i) => i[0] === SOURCE_LOCALE)[1]; + const result = {}; + for (const [locale, strings] of entries) { + if (locale !== SOURCE_LOCALE) { + const withoutUnchangedStrings = removeUnchangedTranslations( + sourceStrings, + strings + ); + const normalizedLocale = locale.toLowerCase().replace(/_/g, "-"); + result[normalizedLocale] = withoutUnchangedStrings; + } + } + return result; +}; + +const run = async () => { + console.log("This is going to take a while."); + + const [runtime, metadata] = await Promise.all([ + downloadAllResourceTranslations(RUNTIME_RESOURCE), + downloadAllResourceTranslations(METADATA_RESOURCE), + ]); + + fs.writeFileSync( + pathUtil.join(__dirname, "../translations/extension-runtime.json"), + JSON.stringify(runtime, null, 4) + ); + + fs.writeFileSync( + pathUtil.join(__dirname, "../translations/extension-metadata.json"), + JSON.stringify(metadata, null, 4) + ); +}; + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/development/homepage-template.ejs b/development/homepage-template.ejs index e89fb72e2c..3d33812f61 100644 --- a/development/homepage-template.ejs +++ b/development/homepage-template.ejs @@ -158,14 +158,15 @@ backdrop-filter: blur(0.6px); } .extension-buttons > * { - padding: 0.5rem; background-color: #4c97ff; + padding: 0.5rem; border-radius: 0.5rem; border: none; font: inherit; cursor: pointer; color: white; text-decoration: none; + text-shadow: 0 0 4px #0003; pointer-events: auto; transition: filter 0.15s; filter: drop-shadow(0px 1px 3px #00000075); @@ -185,11 +186,11 @@ color: white; } .extension-buttons .docs { - background-color: #FFAB19; + background-color: #f69925; color: white; } .extension-buttons .sample { - background-color: #40BF4A; + background-color: #3ebb48; color: white; } .extension-buttons :disabled { diff --git a/development/transifex-common.js b/development/transifex-common.js new file mode 100644 index 0000000000..6dc9ab9468 --- /dev/null +++ b/development/transifex-common.js @@ -0,0 +1,46 @@ +const { transifexApi } = require("@transifex/api"); + +const TOKEN = process.env.TRANSIFEX_TOKEN; +if (!TOKEN) { + console.error("Missing TRANSIFEX_TOKEN."); + process.exit(1); +} + +const ORGANIZATION_NAME = process.env.TRANSIFEX_ORGANIZATION; +if (!ORGANIZATION_NAME) { + console.error("Missing TRANSIFEX_ORGANIZATION."); + process.exit(1); +} + +const PROJECT_NAME = process.env.TRANSIFEX_PROJECT; +if (!PROJECT_NAME) { + console.error("Missing TRANSIFEX_PROJECT."); + process.exit(1); +} + +const RUNTIME_RESOURCE = process.env.TRANSIFEX_RUNTIME_RESOURCE; +if (!RUNTIME_RESOURCE) { + console.error("Missing TRANSIFEX_RUNTIME_RESOURCE."); + process.exit(1); +} + +const METADATA_RESOURCE = process.env.TRANSIFEX_METADATA_RESOURCE; +if (!METADATA_RESOURCE) { + console.error("Missing TRANSIFEX_METADATA_RESOURCE."); + process.exit(1); +} + +const SOURCE_LOCALE = "en"; + +transifexApi.setup({ + auth: TOKEN, +}); + +module.exports = { + transifexApi, + ORGANIZATION_NAME, + PROJECT_NAME, + RUNTIME_RESOURCE, + METADATA_RESOURCE, + SOURCE_LOCALE, +}; diff --git a/development/upload-translations.js b/development/upload-translations.js new file mode 100644 index 0000000000..d35c02301e --- /dev/null +++ b/development/upload-translations.js @@ -0,0 +1,68 @@ +const { + transifexApi, + ORGANIZATION_NAME, + PROJECT_NAME, + RUNTIME_RESOURCE, + METADATA_RESOURCE, +} = require("./transifex-common"); +const Builder = require("./builder"); + +const uploadRuntimeStrings = async (strings) => { + if ( + typeof strings["lab/text@_Animated Text"].string !== "string" || + typeof strings["lab/text@_Animated Text"].developer_comment !== "string" || + Object.keys(strings).length < 500 + ) { + throw new Error("Sanity check failed."); + } + + await transifexApi.ResourceStringsAsyncUpload.upload({ + resource: { + data: { + id: `o:${ORGANIZATION_NAME}:p:${PROJECT_NAME}:r:${RUNTIME_RESOURCE}`, + type: "resources", + }, + }, + content: JSON.stringify(strings), + }); +}; + +const uploadMetadataStrings = async (strings) => { + if ( + typeof strings["lab/text@name"].string !== "string" || + typeof strings["lab/text@name"].developer_comment !== "string" || + Object.keys(strings).length < 100 + ) { + throw new Error("Sanity check failed."); + } + + await transifexApi.ResourceStringsAsyncUpload.upload({ + resource: { + data: { + id: `o:${ORGANIZATION_NAME}:p:${PROJECT_NAME}:r:${METADATA_RESOURCE}`, + type: "resources", + }, + }, + content: JSON.stringify(strings), + }); +}; + +const run = async () => { + console.log("Building..."); + const builder = new Builder(); + const build = builder.build(); + + console.log("Generating strings..."); + const l10n = build.generateL10N(); + + console.log("Uploading runtime strings..."); + await uploadRuntimeStrings(l10n["extension-runtime"]); + + console.log("Uploading metadata strings..."); + await uploadMetadataStrings(l10n["extension-metadata"]); +}; + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/eslint.config.js b/eslint.config.js index 4e26f68ae4..a08bfb3f14 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -111,6 +111,14 @@ module.exports = [ ], 'no-restricted-syntax': [ 'error', + { + selector: 'AssignmentExpression[operator="??="]', + message: 'x ??= y syntax is too new; use x = x ?? y intead' + }, + { + selector: 'MemberExpression[object.name=Object][property.name=hasOwn]', + message: 'Object.hasOwn(...) is too new; use Object.prototype.hasOwnProperty.call(...) instead' + }, { selector: 'CallExpression[callee.name=fetch]', message: 'Use Scratch.fetch() instead of fetch()' diff --git a/extensions/-SIPC-/recording.js b/extensions/-SIPC-/recording.js index 82d2bebb55..c42863f4a2 100644 --- a/extensions/-SIPC-/recording.js +++ b/extensions/-SIPC-/recording.js @@ -96,17 +96,17 @@ return; } console.log("Stop recording"); - mediaRecorder.addEventListener("stop", function () { + mediaRecorder.addEventListener("stop", async function () { const blob = new Blob(recordedChunks, { type: "audio/wav" }); + recordedChunks = []; + const url = URL.createObjectURL(blob); - const downloadLink = document.createElement("a"); - downloadLink.href = url; - downloadLink.download = name; - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); + try { + await Scratch.download(url, name); + } catch (e) { + console.error(e); + } URL.revokeObjectURL(url); - recordedChunks = []; }); mediaRecorder.stop(); mediaRecorder = null; diff --git a/extensions/0832/rxFS.js b/extensions/0832/rxFS.js index 4191496066..331cc5d18a 100644 --- a/extensions/0832/rxFS.js +++ b/extensions/0832/rxFS.js @@ -149,6 +149,7 @@ { blockIconURI: file, opcode: "search", + hideFromPalette: true, blockType: Scratch.BlockType.REPORTER, text: Scratch.translate("search [STR]"), arguments: { @@ -200,8 +201,11 @@ del({ STR }) { str = btoa(unescape(encodeURIComponent(STR))); - rxFSfi[rxFSsy.indexOf(str) + 1 - 1] = undefined; - rxFSsy[rxFSsy.indexOf(str) + 1 - 1] = undefined; + const index = rxFSsy.indexOf(str); + if (index !== -1) { + rxFSfi.splice(index, 1); + rxFSsy.splice(index, 1); + } } file({ STR, STR2 }) { diff --git a/extensions/0832/rxFS2.js b/extensions/0832/rxFS2.js index 4e631709b9..31fad1e381 100644 --- a/extensions/0832/rxFS2.js +++ b/extensions/0832/rxFS2.js @@ -180,6 +180,7 @@ { blockIconURI: folder, opcode: "search", + hideFromPalette: true, blockType: Scratch.BlockType.REPORTER, text: Scratch.translate({ id: "search", default: "search [STR]" }), arguments: { @@ -225,8 +226,11 @@ del({ STR }) { str = encodeURIComponent(STR); - rxFSfi[rxFSsy.indexOf(str) + 1 - 1] = undefined; - rxFSsy[rxFSsy.indexOf(str) + 1 - 1] = undefined; + const index = rxFSsy.indexOf(str); + if (index !== -1) { + rxFSfi.splice(index, 1); + rxFSsy.splice(index, 1); + } } folder({ STR, STR2 }) { diff --git a/extensions/DT/cameracontrols.js b/extensions/DT/cameracontrols.js index 22de6ce907..936ed8aa21 100644 --- a/extensions/DT/cameracontrols.js +++ b/extensions/DT/cameracontrols.js @@ -410,6 +410,29 @@ }, */ "---", + { + opcode: "blocktx", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("stage to world x: [x]"), + arguments: { + x: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100, + }, + }, + }, + { + opcode: "blockty", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("stage to world y: [y]"), + arguments: { + y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100, + }, + }, + }, + "---", { opcode: "changeZoom", blockType: Scratch.BlockType.COMMAND, @@ -544,6 +567,12 @@ getDirection() { return cameraDirection; } + blocktx(args) { + return _translateX(Scratch.Cast.toNumber(args.x)); + } + blockty(args) { + return _translateY(Scratch.Cast.toNumber(args.y)); + } setCol(args, util) { cameraBG = args.val; updateCameraBG(); diff --git a/extensions/Lily/TempVariables2.js b/extensions/Lily/TempVariables2.js index bbc70501cb..812301bf9a 100644 --- a/extensions/Lily/TempVariables2.js +++ b/extensions/Lily/TempVariables2.js @@ -217,13 +217,17 @@ setThreadVariable(args, util) { const thread = util.thread; - thread.variables ??= Object.create(null); + if (!thread.variables) { + thread.variables = Object.create(null); + } thread.variables[args.VAR] = args.STRING; } changeThreadVariable(args, util) { const thread = util.thread; - thread.variables ??= Object.create(null); + if (!thread.variables) { + thread.variables = Object.create(null); + } const vars = thread.variables; const prev = Scratch.Cast.toNumber(vars[args.VAR]); const next = Scratch.Cast.toNumber(args.NUM); @@ -232,21 +236,29 @@ getThreadVariable(args, util) { const thread = util.thread; - thread.variables ??= Object.create(null); + if (!thread.variables) { + thread.variables = Object.create(null); + } return thread.variables[args.VAR] ?? ""; } threadVariableExists(args, util) { const thread = util.thread; - thread.variables ??= Object.create(null); - return Object.hasOwn(thread.variables, args.VAR); + if (!thread.variables) { + thread.variables = Object.create(null); + } + return Object.prototype.hasOwnProperty.call(thread.variables, args.VAR); } forEachThreadVariable(args, util) { const thread = util.thread; - thread.variables ??= Object.create(null); + if (!thread.variables) { + thread.variables = Object.create(null); + } const vars = thread.variables; - util.stackFrame.index ??= 0; + if (!Object.prototype.hasOwnProperty.call(util.stackFrame, "index")) { + util.stackFrame.index = 0; + } if (util.stackFrame.index < Number(args.NUM)) { util.stackFrame.index++; vars[args.VAR] = util.stackFrame.index; @@ -256,7 +268,9 @@ listThreadVariables(args, util) { const thread = util.thread; - thread.variables ??= Object.create(null); + if (!thread.variables) { + thread.variables = Object.create(null); + } return Object.keys(thread.variables).join(","); } @@ -277,7 +291,10 @@ } runtimeVariableExists(args) { - return Object.hasOwn(this.runtimeVariables, args.VAR); + return Object.prototype.hasOwnProperty.call( + this.runtimeVariables, + args.VAR + ); } listRuntimeVariables(args, util) { @@ -292,8 +309,9 @@ this.runtimeVariables = Object.create(null); } } - // The expose format follows scratch's convention of `ext_${extensionId}`. + // The expose format follows TurboWarp's convention of `ext_${extensionId}`. // Expose the extension on runtime for others to use. - Scratch.vm.runtime.ext_lmsTempVars2 = new TempVars(); - Scratch.extensions.register(Scratch.vm.runtime.ext_lmsTempVars2); + const extension = new TempVars(); + Scratch.vm.runtime.ext_lmsTempVars2 = extension; + Scratch.extensions.register(extension); })(Scratch); diff --git a/extensions/Xeltalliv/simple3D.js b/extensions/Xeltalliv/simple3D.js index 93511b5e6f..11c629277e 100644 --- a/extensions/Xeltalliv/simple3D.js +++ b/extensions/Xeltalliv/simple3D.js @@ -3,7 +3,7 @@ // Description: Make GPU accelerated 3D projects easily. // By: Vadik1 // License: MPL-2.0 AND BSD-3-Clause -// Version: 1.2.0 +// Version: 1.2.1 (function (Scratch) { "use strict"; @@ -352,6 +352,15 @@ gl.scissor(b.x, b.y, b.w, b.h); } } + getReadarea() { + if (this.readarea) return this.readarea; + return { + x: 0, + y: 0, + w: this.width, + h: this.height, + }; + } updateDepth() { if (this.depthTest == "everything" && !this.depthWrite) { gl.disable(gl.DEPTH_TEST); @@ -1832,7 +1841,10 @@ void main() { text: "Open extra resources", func: "openSite", def: function () { - Scratch.openWindow("https://xeltalliv.github.io/simple3d-extension/"); + // Exempted from Scratch.openWindow as initiated by user gesture. + // docsURI won't ask for permission so it doesn't make sense for this to either. + // eslint-disable-next-line no-restricted-syntax + window.open("https://xeltalliv.github.io/simple3d-extension/"); }, }, { @@ -4310,7 +4322,7 @@ void main() { ); if (!list) return; if (!currentRenderTarget.checkIfValid()) return; - const { x, y, w, h } = currentRenderTarget.readarea; + const { x, y, w, h } = currentRenderTarget.getReadarea(); if (w == 0 || h == 0) return; const pixels = new Uint8ClampedArray(w * h * 4); gl.readPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); @@ -4344,7 +4356,7 @@ void main() { return currentRenderTarget.hasDepthBuffer; if (PROPERTY == "image as data URI") { if (!currentRenderTarget.checkIfValid()) return ""; - const { x, y, w, h } = currentRenderTarget.readarea; + const { x, y, w, h } = currentRenderTarget.getReadarea(); if (w == 0 || h == 0) return ""; const pixels = new Uint8ClampedArray(w * h * 4); gl.readPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); diff --git a/extensions/files.js b/extensions/files.js index af703b8028..f21616f395 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -215,26 +215,18 @@ } }); - /** - * @param {string} url a data:, blob:, or same-origin URL - * @param {string} file - */ - const downloadURL = (url, file) => { - const link = document.createElement("a"); - link.href = url; - link.download = file; - document.body.appendChild(link); - link.click(); - link.remove(); - }; - /** * @param {Blob} blob Data to download * @param {string} file Name of the file + * @returns {Promise} */ - const downloadBlob = (blob, file) => { + const downloadBlob = async (blob, file) => { const url = URL.createObjectURL(blob); - downloadURL(url, file); + try { + await Scratch.download(url, file); + } catch (e) { + console.error(e); + } URL.revokeObjectURL(url); }; @@ -255,17 +247,16 @@ * @param {string} url * @param {string} file */ - const downloadUntrustedURL = (url, file) => { - // Don't want to return a Promise here when not actually needed + const downloadUntrustedURL = async (url, file) => { if (isDataURL(url)) { - downloadURL(url, file); - } else { - return Scratch.fetch(url) - .then((res) => res.blob()) - .then((blob) => { - downloadBlob(blob, file); - }); + // TODO: Scratch.fetch's better handling of data: means this is probably not needed anymore + // and it the blob: probably works better with big files + return Scratch.download(url, file); } + + const res = await Scratch.fetch(url); + const blob = await res.blob(); + await downloadBlob(blob, file); }; class Files { @@ -424,18 +415,26 @@ return showFilePrompt(args.extension, args.as); } - download(args) { - downloadBlob( - new Blob([Scratch.Cast.toString(args.text)]), - Scratch.Cast.toString(args.file) - ); + async download(args) { + try { + await downloadBlob( + new Blob([Scratch.Cast.toString(args.text)]), + Scratch.Cast.toString(args.file) + ); + } catch (e) { + console.error(e); + } } - downloadURL(args) { - return downloadUntrustedURL( - Scratch.Cast.toString(args.url), - Scratch.Cast.toString(args.file) - ); + async downloadURL(args) { + try { + await downloadUntrustedURL( + Scratch.Cast.toString(args.url), + Scratch.Cast.toString(args.file) + ); + } catch (e) { + console.error(e); + } } setOpenMode(args) { diff --git a/extensions/godslayerakp/http.js b/extensions/godslayerakp/http.js index b552c6560e..eb1a977492 100644 --- a/extensions/godslayerakp/http.js +++ b/extensions/godslayerakp/http.js @@ -388,7 +388,7 @@ { opcode: "setBodyToForm", blockType: BlockType.COMMAND, - text: Scratch.translate("set request body to a form"), + text: Scratch.translate("set request body to multipart form"), }, { opcode: "getFormProperty", @@ -399,7 +399,7 @@ defaultValue: "name", }, }, - text: Scratch.translate("[name] in request form"), + text: Scratch.translate("[name] in multipart form"), }, { opcode: "setFormProperty", @@ -414,7 +414,7 @@ defaultValue: "value", }, }, - text: Scratch.translate("set [name] to [value] in request form"), + text: Scratch.translate("set [name] to [value] in multipart form"), }, { opcode: "deleteFormProperty", @@ -425,7 +425,7 @@ defaultValue: "name", }, }, - text: Scratch.translate("delete [name] from request form"), + text: Scratch.translate("delete [name] from multipart form"), }, "---", { @@ -520,10 +520,11 @@ }, mimeType: { items: [ + "application/json", + "application/x-www-form-urlencoded", "application/javascript", "application/ogg", "application/pdf", - "application/json", "application/ld+json", "application/xml", "application/zip", diff --git a/extensions/obviousAlexC/SensingPlus.js b/extensions/obviousAlexC/SensingPlus.js index fb9d94c9b0..4e0c066d9a 100644 --- a/extensions/obviousAlexC/SensingPlus.js +++ b/extensions/obviousAlexC/SensingPlus.js @@ -7,16 +7,15 @@ (function (Scratch) { "use strict"; - //put these back here so I don't have to define scratch.cast again. - let notMobile = false; - - /* globals Accelerometer, Gyro */ - const SpeechRecognition = + // @ts-expect-error typeof webkitSpeechRecognition !== "undefined" - ? window.webkitSpeechRecognition - : typeof window.SpeechRecognition !== "undefined" - ? window.SpeechRecognition + ? // @ts-expect-error + window.webkitSpeechRecognition + : // @ts-expect-error + typeof window.SpeechRecognition !== "undefined" + ? // @ts-expect-error + window.SpeechRecognition : null; let recognizedSpeech = ""; @@ -64,135 +63,278 @@ }); }; - let initializedSensors = false; - const deviceVelocity = { - x: 0, - y: 0, - z: 0, + const physicalDeviceState = { + accelerationX: 0, + accelerationY: 0, + accelerationZ: 0, rotationX: 0, rotationY: 0, rotationZ: 0, }; - const deviceStatus = { - gyroscope: false, + const sensorStatus = { accelerometer: false, + gyroscope: false, }; - const initializeSensors = () => { - if (initializedSensors) { - return; - } - initializedSensors = true; + /** + * @returns {boolean} + */ + const sensorAccessRequiresPermission = () => + typeof DeviceMotionEvent === "function" && + // @ts-expect-error + typeof DeviceMotionEvent.requestPermission === "function"; - if (typeof Accelerometer !== "function") { - try { - const accelerometer = new Accelerometer({ - referenceFrame: "device", - }); - accelerometer.addEventListener("error", (e) => { - console.error("accelerometer error", e.error); - deviceStatus.accelerometer = false; - }); - accelerometer.addEventListener("reading", () => { - deviceVelocity.x = accelerometer.x; - deviceVelocity.y = accelerometer.y; - deviceVelocity.z = accelerometer.z; - deviceStatus.accelerometer = true; + /** + * Assumes you already checked sensorAccessRequiresPermission() === true. + * @returns {Promise<'granted'|'denied'|'unknown'>} Will never reject. + */ + const requestSensorPermission = () => { + // @ts-expect-error + return DeviceMotionEvent.requestPermission().catch((error) => { + // Usually this means we weren't in a user gesture. + console.error(error); + return "unknown"; + }); + }; + + /** + * Assumes you already checked sensorAccessRequiresPermission() === true. + * @returns {Promise} + */ + const askUserForSensorPermission = async () => { + // Safari automatically denies any request not made directly in a user gesture handler, + // so this request will almost certainly fail. We'll still try though, just in case. + let status = await requestSensorPermission(); + + if (status === "unknown") { + status = await new Promise((resolve) => { + const outer = document.createElement("div"); + outer.style.width = "100%"; + outer.style.height = "100%"; + outer.style.display = "flex"; + outer.style.alignItems = "center"; + outer.style.justifyContent = "center"; + outer.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; + outer.style.backdropFilter = "blur(10px)"; + outer.style.pointerEvents = "auto"; + outer.tabIndex = 0; + + const inner = document.createElement("div"); + inner.textContent = Scratch.translate( + "Tap to allow access to accelerometer and gyroscope." + ); + inner.style.maxWidth = "360px"; + inner.style.color = "white"; + inner.style.textAlign = "center"; + outer.appendChild(inner); + + outer.addEventListener("click", () => { + resolve(requestSensorPermission()); + Scratch.renderer.removeOverlay(outer); }); - accelerometer.start(); - } catch (e) { - console.error("error setting up accelerometer", e); - } - } else { - console.warn("accelerometer API is not supported in this browser"); + + Scratch.renderer.addOverlay(outer, "scale"); + }); } - if (typeof Gyro !== "undefined") { - try { - const gyro = new Gyro({ - frequency: 30, - }); - gyro.addEventListener("error", (e) => { - console.error("gyro error", e.error); - deviceStatus.gyroscope = false; - }); - gyro.addEventListener("reading", () => { - deviceVelocity.rotationX = gyro.x; - deviceVelocity.rotationY = gyro.y; - deviceVelocity.rotationZ = gyro.z; - deviceStatus.gyroscope = true; - }); - } catch (e) { - console.error("error setting up gyro", e); - } - } else { - console.warn("gyro API is not supported in this browser"); + if (status === "denied") { + // Requesting permission again will be ignored no matter what. + // The flow for resetting this is awful, so let's at least tell the user how to do that. + alert( + Scratch.translate( + "To allow accelerometer and gyroscope access, open iOS settings > Apps > Safari > Advanced > Website Data > press Edit > Clear data for {domain}, then refresh this page.", + { + domain: location.hostname, + } + ) + ); } + + const granted = status === "granted"; + sensorStatus.accelerometer = granted; + sensorStatus.gyroscope = granted; }; + /** @type {null|Promise} */ + let initializingSensorsPromise = null; + /** @type {boolean} */ + let askedForSensorPermission = false; + + /** + * @template T + * @param {() => T} callback + * @returns {T|Promise} + */ + const whenSensorsInitialized = (callback) => { + if (!sensorAccessRequiresPermission() || askedForSensorPermission) { + return callback(); + } + + if (!initializingSensorsPromise) { + initializingSensorsPromise = askUserForSensorPermission().then(() => { + // Whether we got permission or not, asking again won't change the result. + askedForSensorPermission = true; + }); + } + + return initializingSensorsPromise.then(callback); + }; + + window.addEventListener("devicemotion", (event) => { + // On desktops, this event is fired with nulls. + if ( + event.accelerationIncludingGravity.x !== null && + event.accelerationIncludingGravity.y !== null && + event.accelerationIncludingGravity.z !== null + ) { + askedForSensorPermission = true; + sensorStatus.accelerometer = true; + physicalDeviceState.accelerationX = event.accelerationIncludingGravity.x; + physicalDeviceState.accelerationY = event.accelerationIncludingGravity.y; + physicalDeviceState.accelerationZ = event.accelerationIncludingGravity.z; + } + }); + + window.addEventListener("deviceorientation", (event) => { + // On desktops, this event is fired with nulls. + if (event.alpha !== null && event.beta !== null && event.gamma !== null) { + askedForSensorPermission = true; + sensorStatus.gyroscope = true; + physicalDeviceState.rotationX = event.beta; + physicalDeviceState.rotationY = event.gamma; + physicalDeviceState.rotationZ = event.alpha; + } + }); + const vm = Scratch.vm; const runtime = vm.runtime; const canvas = runtime.renderer.canvas; - let fingersDown = 0; - const lastFingerPositions = []; - const fingerPositions = []; + const maxTouchPoints = navigator.maxTouchPoints; + const supportsTouches = maxTouchPoints > 0; + + /** + * Maps system Touch identifiers to the identifers we expose to projects. + * This is necessary because Safari uses incremental IDs that only ever go up, + * so they get very big and won't start from 0. + * @type {Map} + */ + const nativeTouchIdToScratchId = new Map(); + + /** + * @typedef ScratchFinger + * @property {number} x + * @property {number} y + * @property {number} lastX + * @property {number} lastY + */ + + /** + * Maps Scratch touch ID to internal object. + * @type {Map} + */ + const scratchFingers = new Map(); + + /** + * @returns {number} A positive integer. + */ + const getUnusedScratchId = () => { + // This is slower than it could be but this doesn't run enough to matter. + // IDs start from 1, like Scratch lists. + let i = 1; + while (scratchFingers.has(i)) { + i++; + } + return i; + }; /** @param {TouchEvent} event */ - function handleTouchStart(event) { + const handleTouchStart = (event) => { event.preventDefault(); - const changedTouches = event.changedTouches; - const changedTouchesKeys = Object.keys(changedTouches); + const canvasPos = canvas.getBoundingClientRect(); - fingersDown = event.touches.length; - - changedTouchesKeys.forEach((touch) => { - lastFingerPositions[changedTouches[touch].identifier] = [ - changedTouches[touch].clientX - canvasPos.left, - changedTouches[touch].clientY - canvasPos.top, - ]; - fingerPositions[changedTouches[touch].identifier] = [ - changedTouches[touch].clientX - canvasPos.left, - changedTouches[touch].clientY - canvasPos.top, - ]; - }); - } + for (const touch of event.changedTouches) { + const nextAvailableScratchId = getUnusedScratchId(); + nativeTouchIdToScratchId.set(touch.identifier, nextAvailableScratchId); + + const x = touch.clientX - canvasPos.left; + const y = touch.clientY - canvasPos.top; + scratchFingers.set(nextAvailableScratchId, { + x: x, + y: y, + lastX: x, + lastY: y, + }); + } + }; /** @param {TouchEvent} event */ - function handleTouchMove(event) { + const handleTouchMove = (event) => { event.preventDefault(); - const changedTouches = event.changedTouches; + const canvasPos = canvas.getBoundingClientRect(); - const changedTouchesKeys = Object.keys(changedTouches); - fingersDown = event.touches.length; - changedTouchesKeys.forEach((touch) => { - lastFingerPositions[changedTouches[touch].identifier] = [ - fingerPositions[changedTouches[touch].identifier][0], - fingerPositions[changedTouches[touch].identifier][1], - ]; - fingerPositions[changedTouches[touch].identifier] = [ - changedTouches[touch].clientX - canvasPos.left, - changedTouches[touch].clientY - canvasPos.top, - ]; - }); - } + for (const touch of event.changedTouches) { + const scratchId = nativeTouchIdToScratchId.get(touch.identifier); + const finger = scratchFingers.get(scratchId); + finger.lastX = finger.x; + finger.lastY = finger.y; + finger.x = touch.clientX - canvasPos.left; + finger.y = touch.clientY - canvasPos.top; + } + }; /** @param {TouchEvent} event */ - function handleTouchEnd(event) { + const handleTouchEnd = (event) => { event.preventDefault(); - const changedTouches = event.changedTouches; - const changedTouchesKeys = Object.keys(changedTouches); - fingersDown = event.touches.length; - changedTouchesKeys.forEach((touch) => { - lastFingerPositions[changedTouches[touch].identifier] = null; - fingerPositions[changedTouches[touch].identifier] = null; - }); - } - canvas.addEventListener("touchstart", handleTouchStart, false); - canvas.addEventListener("touchmove", handleTouchMove, false); - canvas.addEventListener("touchcancel", handleTouchEnd, false); - canvas.addEventListener("touchend", handleTouchEnd, false); + for (const touch of event.changedTouches) { + const scratchId = nativeTouchIdToScratchId.get(touch.identifier); + scratchFingers.delete(scratchId); + nativeTouchIdToScratchId.delete(touch.identifier); + } + }; + + /** + * @param {VM.Target} target + * @returns {number} -1 if not touching, else the Scratch ID + */ + const findAnyTouchingFinger = (target) => { + for (const [scratchId, finger] of scratchFingers.entries()) { + const touching = target.isTouchingPoint(finger.x, finger.y); + if (touching) { + return scratchId; + } + } + return -1; + }; + + /** + * @param {VM.Target} target + * @param {number} scratchId + * @returns {boolean} + */ + const isTouchingSpecificFinger = (target, scratchId) => { + const finger = scratchFingers.get(scratchId); + return !!finger && target.isTouchingPoint(finger.x, finger.y); + }; + + canvas.addEventListener("touchstart", handleTouchStart, { + passive: false, + }); + canvas.addEventListener("touchmove", handleTouchMove, { + passive: false, + }); + canvas.addEventListener("touchcancel", handleTouchEnd, { + passive: false, + }); + canvas.addEventListener("touchend", handleTouchEnd, { + passive: false, + }); + + const fingersMenu = []; + for (let i = 0; i < Math.max(maxTouchPoints, 10); i++) { + fingersMenu.push((i + 1).toString()); + } /** * @param {string} listData @@ -239,44 +381,6 @@ const layerIco = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI0NS45NzY4OCIgaGVpZ2h0PSI0Ni40NzQ2NiIgdmlld0JveD0iMCwwLDQ1Ljk3Njg4LDQ2LjQ3NDY2Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjE1LjAzOTI5LC0xNTguOTgzODMpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxnIGZpbGwtcnVsZT0iZXZlbm9kZCI+PHBhdGggZD0iTTIzOS4xODkyOSwxNjUuNDc3MjNjMC4xNSwtMC4xIDAuNCwtMC4wNSAwLjQ1LDAuMTVsMS4zLDUuMzVjMCwwIDMuMiwyLjM1IDQuMTUsNGMxLjYsMi43NSAxLjY1LDUgMS42NSw1YzAsMCAzLjU1LDEuMDUgNC4xNSwzLjljMC42LDIuODUgLTEuNiw4LjI1IC0xMSwxMC4xYy05LjQsMS44NSAtMTYuOTUsLTAuNyAtMjAuNSwtNi40Yy0zLjU1LC01LjcgMi4wNSwtMTIuNSAxLjc1LC0xMi4xbC0xLjA1LC04Ljk1Yy0wLjA1LC0wLjIgMC4yLC0wLjM1IDAuNCwtMC4yNWw2LjA1LDMuOTVjMCwwIDIuMjUsLTAuODUgNC42LC0wLjk1YzEuNCwtMC4xIDIuNiwwIDMuNzUsMC4yeiIgZmlsbD0iIzNiYTJjZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMjQ2LjU4OTI5LDE4MC4xNzcyM2MwLDAgMy40NSwwLjkgNC4wNSwzLjc1YzAuNiwyLjg1IC0xLjgsOCAtMTEuMSw5LjhjLTEyLjEsMi41IC0xNy44NSwtNC43IC0xNC41LC0xMGMzLjM1LC01LjM1IDkuMSwtMC44IDEzLjMsLTEuMWMzLjYsLTAuMjUgNCwtMy40IDguMjUsLTIuNDV6IiBmaWxsPSIjYTdlMmZiIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMjU1Ljc4OTI5LDE4MC43MjcyM2MtMi4zNSwxLjkgLTUuOTUsMS45NSAtNS45NSwxLjk1IiBmaWxsPSJub25lIiBzdHJva2U9IiMxYjU1NmUiIHN0cm9rZS13aWR0aD0iMS4yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMjU1LjEzOTI5LDE4Ni4zMjcyM2MtMy4xNSwwLjI1IC01LjEsLTAuNyAtNS4xLC0wLjciIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzFiNTU2ZSIgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yMzguMTM5MjksMTgxLjMyNzIzYzEuMDUsMCAyLjE1LDAuMSAyLjIsMC40NWMwLjA1LDAuNyAtMC43LDIuMSAtMS41LDIuMTVjLTAuOSwwLjEgLTMsLTEuMTUgLTMsLTEuOTVjLTAuMDUsLTAuNiAxLjMsLTAuNjUgMi4zLC0wLjY1eiIgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTIxNS42MzkyOSwxODAuNTc3MjNjMCwwIDQuMywxLjQgNi4wNSwyLjk1IiBmaWxsPSJub25lIiBzdHJva2U9IiMxYjU1NmUiIHN0cm9rZS13aWR0aD0iMS4yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMjIxLjgzOTI5LDE4NS4yNzcyM2MtMi4xNSwwLjg1IC01Ljg1LDAuMyAtNS44NSwwLjMiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzFiNTU2ZSIgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxnIGZpbGw9IiMxYjU1NmUiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciI+PHBhdGggZD0iTTI0My44MzkyOSwxNzguMzI3MjNjMCwwLjU1IC0wLjQsMSAtMC45LDFjLTAuNSwwIC0wLjksLTAuNDUgLTAuOSwtMWMwLC0wLjU1IDAuNCwtMSAwLjksLTFjMC41LDAgMC45LDAuNDUgMC45LDEiLz48L2c+PGcgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIj48cGF0aCBkPSJNMjMxLjMzOTI5LDE3OS43NzcyM2MwLDAuNTUgLTAuNCwxIC0wLjksMWMtMC41LDAgLTAuOSwtMC40NSAtMC45LC0xYzAsLTAuNTUgMC40LC0xIDAuOSwtMWMwLjUsMC4wNSAwLjksMC40NSAwLjksMSIvPjwvZz48L2c+PHBhdGggZD0iTTIxOC45ODM4MywyMDEuMDE2MTl2LTQyLjAzMjM1aDQyLjAzMjM1djQyLjAzMjM1eiIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMCIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48ZyBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yNDMuNDc1LDE3NS43NjI5NWMwLjE1LC0wLjEgMC40LC0wLjA1IDAuNDUsMC4xNWwxLjMsNS4zNWMwLDAgMy4yLDIuMzUgNC4xNSw0YzEuNiwyLjc1IDEuNjUsNSAxLjY1LDVjMCwwIDMuNTUsMS4wNSA0LjE1LDMuOWMwLjYsMi44NSAtMS42LDguMjUgLTExLDEwLjFjLTkuNCwxLjg1IC0xNi45NSwtMC43IC0yMC41LC02LjRjLTMuNTUsLTUuNyAyLjA1LC0xMi41IDEuNzUsLTEyLjFsLTEuMDUsLTguOTVjLTAuMDUsLTAuMiAwLjIsLTAuMzUgMC40LC0wLjI1bDYuMDUsMy45NWMwLDAgMi4yNSwtMC44NSA0LjYsLTAuOTVjMS40LC0wLjEgMi42LDAgMy43NSwwLjJ6IiBmaWxsPSIjM2JhMmNlIiBzdHJva2U9IiMxYjU1NmUiIHN0cm9rZS13aWR0aD0iMS4yIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPjxwYXRoIGQ9Ik0yNTAuODc1LDE5MC40NjI5NWMwLDAgMy40NSwwLjkgNC4wNSwzLjc1YzAuNiwyLjg1IC0xLjgsOCAtMTEuMSw5LjhjLTEyLjEsMi41IC0xNy44NSwtNC43IC0xNC41LC0xMGMzLjM1LC01LjM1IDkuMSwtMC44IDEzLjMsLTEuMWMzLjYsLTAuMjUgNCwtMy40IDguMjUsLTIuNDV6IiBmaWxsPSIjYTdlMmZiIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMjYwLjA3NSwxOTEuMDEyOTVjLTIuMzUsMS45IC01Ljk1LDEuOTUgLTUuOTUsMS45NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTI1OS40MjUsMTk2LjYxMjk1Yy0zLjE1LDAuMjUgLTUuMSwtMC43IC01LjEsLTAuNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTI0Mi40MjUsMTkxLjYxMjk1YzEuMDUsMCAyLjE1LDAuMSAyLjIsMC40NWMwLjA1LDAuNyAtMC43LDIuMSAtMS41LDIuMTVjLTAuOSwwLjEgLTMsLTEuMTUgLTMsLTEuOTVjLTAuMDUsLTAuNiAxLjMsLTAuNjUgMi4zLC0wLjY1eiIgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTIxOS45MjUsMTkwLjg2Mjk1YzAsMCA0LjMsMS40IDYuMDUsMi45NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTIyNi4xMjUsMTk1LjU2Mjk1Yy0yLjE1LDAuODUgLTUuODUsMC4zIC01Ljg1LDAuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWI1NTZlIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PGcgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIj48cGF0aCBkPSJNMjQ4LjEyNSwxODguNjEyOTVjMCwwLjU1IC0wLjQsMSAtMC45LDFjLTAuNSwwIC0wLjksLTAuNDUgLTAuOSwtMWMwLC0wLjU1IDAuNCwtMSAwLjksLTFjMC41LDAgMC45LDAuNDUgMC45LDEiLz48L2c+PGcgZmlsbD0iIzFiNTU2ZSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIj48cGF0aCBkPSJNMjM1LjYyNSwxOTAuMDYyOTVjMCwwLjU1IC0wLjQsMSAtMC45LDFjLTAuNSwwIC0wLjksLTAuNDUgLTAuOSwtMWMwLC0wLjU1IDAuNCwtMSAwLjksLTFjMC41LDAuMDUgMC45LDAuNDUgMC45LDEiLz48L2c+PC9nPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjI0Ljk2MDcwOTI4NTcxNDMzOjIxLjAxNjE2NS0tPg=="; - const userAgent = navigator.userAgent; - let supportsTouches = true; - if ( - userAgent.includes("Safari") && - /^((?!chrome|android).)*safari/i.test(userAgent) - ) { - //* Its a problem with all safari browsers from what I see now which is odd since apple says its supported? - supportsTouches = false; - } else if ( - userAgent.includes("Windows") || - userAgent.includes("Mac OS") || - userAgent.includes("Linux") || - userAgent.includes("CrOS") - ) { - //* <-- Most chrome OS devices support touch events with up to 10 fingers but include a check to make it better anyways. - notMobile = true; - supportsTouches = navigator.maxTouchPoints > 0; - } - - const maxTouchPoints = navigator.maxTouchPoints; - - function makeArrayOfTouches() { - let TPArray = []; - if (maxTouchPoints == 0 || notMobile) { - //*For non touch compatible devices - for (let TP = 0; TP < 10; TP++) { - TPArray.push(Scratch.Cast.toString(TP + 1)); - } - } else { - for (let TP = 0; TP < maxTouchPoints; TP++) { - TPArray.push(Scratch.Cast.toString(TP + 1)); - } - } - return TPArray; - } - - const touchPointsArray = makeArrayOfTouches(); //* <-- Do this for devices that really can't support that many touches. - class SensingPlus { getInfo() { return { @@ -287,14 +391,6 @@ id: "obviousalexsensing", name: Scratch.translate("Sensing+"), blocks: [ - { - blockType: "label", - text: Scratch.translate("Touch blocks are broken in Safari."), - }, - { - blockType: "label", - text: Scratch.translate("We will try to fix them soon."), - }, { opcode: "supportsTouches", blockType: Scratch.BlockType.BOOLEAN, @@ -631,12 +727,12 @@ opcode: "getDeviceSpeed", blockIconURI: deviceVelIco, blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate("[type] speed on the [axis] axis"), + text: Scratch.translate("[type] on the [axis] axis"), disableMonitor: true, arguments: { type: { type: Scratch.ArgumentType.STRING, - menu: "velocitymenu", + menu: "velocitymenu", // velocitymenu is poorly named }, axis: { type: Scratch.ArgumentType.STRING, @@ -649,7 +745,7 @@ menus: { fingerIDMenu: { acceptReporters: true, - items: touchPointsArray, + items: fingersMenu, }, deviceMenu: { acceptReporters: true, @@ -672,15 +768,16 @@ acceptReporters: true, items: ["x", "y", "z"], }, + // velocitymenu is poorly named. velocitymenu: { acceptReporters: true, items: [ { - text: Scratch.translate("positional"), + text: Scratch.translate("positional acceleration"), value: "positional", }, { - text: Scratch.translate("rotational"), + text: Scratch.translate("rotation rate"), value: "rotational", }, ], @@ -742,61 +839,6 @@ }; } - _touchUtil = { - blankExpression: () => {}, - isTouchingAnyFinger(util, success, fail) { - success = success || this.blankExpression; - if (success == null) { - success = this.blankExpression; - } - fail = fail || this.blankExpression; - if (fail == null) { - fail = this.blankExpression; - } - - for (let index = 0; index < fingerPositions.length; index++) { - const fingerPos = fingerPositions[index]; - if (fingerPos != null) { - const touching = util.target.isTouchingPoint( - fingerPos[0], - fingerPos[1] - ); - if (touching) { - success(index); - return true; - } - } - } - fail(-1); - return false; - }, - - isTouchingSpecificFinger(id, util, success, fail) { - success = success || this.blankExpression; - if (success == null) { - success = this.blankExpression; - } - fail = fail || this.blankExpression; - if (fail == null) { - fail = this.blankExpression; - } - - const fingerPos = fingerPositions[Scratch.Cast.toNumber(id) - 1]; - if (fingerPos != null) { - const touching = util.target.isTouchingPoint( - fingerPos[0], - fingerPos[1] - ); - if (touching) { - success(id); - return true; - } - } - fail(id); - return false; - }, - }; - supportsTouches() { return supportsTouches; } @@ -806,16 +848,16 @@ } getFingerSpeed({ ID }) { - const fingerPos = fingerPositions[ID - 1]; - const fingerLastPos = lastFingerPositions[ID - 1]; - if (!fingerPos || !fingerLastPos) { + const finger = scratchFingers.get(Scratch.Cast.toNumber(ID)); + if (!finger) { return 0; } const speed = Math.sqrt( - Math.pow(fingerPos[0] - fingerLastPos[0], 2) + - Math.pow(fingerPos[1] - fingerLastPos[1], 2) + Math.pow(finger.x - finger.lastX, 2) + + Math.pow(finger.y - finger.lastY, 2) ); - lastFingerPositions[ID - 1] = [fingerPos[0], fingerPos[1]]; + finger.lastX = finger.x; + finger.lastY = finger.y; return speed; } @@ -878,33 +920,36 @@ } hasDevice({ device }) { - if (deviceStatus[device]) { - return deviceStatus[device]; - } - return false; + return whenSensorsInitialized(() => { + if (Object.prototype.hasOwnProperty.call(sensorStatus, device)) { + return sensorStatus[device]; + } + return false; + }); } getDeviceSpeed({ type, axis }) { - initializeSensors(); - if (type === "positional") { - if (axis === "x") { - return deviceVelocity.x; - } else if (axis === "y") { - return deviceVelocity.y; - } else if (axis === "z") { - return deviceVelocity.z; - } - } else if (type === "rotational") { - if (axis === "x") { - return deviceVelocity.rotationX; - } else if (axis === "y") { - return deviceVelocity.rotationY; - } else if (axis === "z") { - return deviceVelocity.rotationZ; + return whenSensorsInitialized(() => { + if (type === "positional") { + if (axis === "x") { + return physicalDeviceState.accelerationX; + } else if (axis === "y") { + return physicalDeviceState.accelerationY; + } else if (axis === "z") { + return physicalDeviceState.accelerationZ; + } + } else if (type === "rotational") { + if (axis === "x") { + return physicalDeviceState.rotationX; + } else if (axis === "y") { + return physicalDeviceState.rotationY; + } else if (axis === "z") { + return physicalDeviceState.rotationZ; + } } - } - // should never happen - return 0; + // should never happen + return 0; + }); } getClipBoard() { @@ -926,7 +971,7 @@ } getFingersTouching() { - return fingersDown; + return scratchFingers.size; } getSprites() { @@ -979,49 +1024,40 @@ } touchingFinger(args, util) { - return this._touchUtil.isTouchingAnyFinger(util); + return findAnyTouchingFinger(util.target) !== -1; } touchingSpecificFinger({ ID }, util) { - return this._touchUtil.isTouchingSpecificFinger(ID, util); + return isTouchingSpecificFinger(util.target, Scratch.Cast.toNumber(ID)); } getTouchingFingerID(args, util) { - let TouchingFingerID = 0; - this._touchUtil.isTouchingAnyFinger(util, (FID) => { - TouchingFingerID = FID + 1; - }); - return TouchingFingerID; + const touching = findAnyTouchingFinger(util.target); + if (touching === -1) { + return 0; + } + return touching; } fingerPosition({ ID, PositionType }) { - const index = Scratch.Cast.toNumber(ID) - 1; - const fingerPos = fingerPositions[index]; - if (fingerPos) { - const positionIndex = PositionType === "x" ? 0 : 1; - const finger = [fingerPos[0], fingerPos[1]]; - let scratchCoords = finger; - const runtime = Scratch.vm.runtime; - + const finger = scratchFingers.get(Scratch.Cast.toNumber(ID)); + if (finger) { const canvasRect = canvas.getBoundingClientRect(); - if (PositionType === "x") { + if (Scratch.Cast.toString(PositionType) === "x") { const clientWidth = canvasRect.right - canvasRect.left; const toScratch = runtime.stageWidth / clientWidth; - scratchCoords[0] *= toScratch; - scratchCoords[0] -= runtime.stageWidth / 2; + return finger.x * toScratch - runtime.stageWidth / 2; } else { const clientheight = canvasRect.bottom - canvasRect.top; const toScratch = runtime.stageHeight / clientheight; - scratchCoords[1] *= toScratch; - scratchCoords[1] = runtime.stageHeight / 2 - scratchCoords[1]; + return runtime.stageHeight / 2 - finger.y * toScratch; } - return scratchCoords[positionIndex]; } return 0; } isFingerDown({ ID }) { - return !!fingerPositions[Scratch.Cast.toNumber(ID) - 1]; + return scratchFingers.has(Scratch.Cast.toNumber(ID)); } listInSprite({ index, List }) { diff --git a/extensions/obviousAlexC/penPlus.js b/extensions/obviousAlexC/penPlus.js index 688b1e1e53..826ef54ece 100644 --- a/extensions/obviousAlexC/penPlus.js +++ b/extensions/obviousAlexC/penPlus.js @@ -3917,9 +3917,6 @@ //?Renderer Freaks out if we don't do this so do it. - //trying my best to reduce memory usage - gl.viewport(0, 0, nativeSize[0], nativeSize[1]); - //Paratheses because I know some obscure browser will screw this up. x1 = Scratch.Cast.toNumber(x1); x2 = Scratch.Cast.toNumber(x2); @@ -3957,9 +3954,6 @@ //?Renderer Freaks out if we don't do this so do it. - //trying my best to reduce memory usage - gl.viewport(0, 0, nativeSize[0], nativeSize[1]); - //Paratheses because I know some obscure browser will screw this up. x1 = Scratch.Cast.toNumber(x1); x2 = Scratch.Cast.toNumber(x2); @@ -4077,7 +4071,7 @@ if (costIndex >= 0) { const curCostume = curTarget.sprite.costumes[costIndex].asset.encodeDataURI(); - return curCostume; + return curCostume || 0; } } @@ -4085,7 +4079,7 @@ //Just a simple thing to allow for pen drawing const costIndex = this.penPlusCostumeLibrary[costume]; if (costIndex) { - return costIndex[dimension]; + return costIndex[dimension] || ""; } } @@ -4142,11 +4136,13 @@ y = Math.floor(y - 1); const colorIndex = (y * curCostume.width + x) * 4; if (textureData[colorIndex] && x < curCostume.width && x >= 0) { - return this.colorLib.rgbtoSColor({ - R: textureData[colorIndex] / 2.55, - G: textureData[colorIndex + 1] / 2.55, - B: textureData[colorIndex + 2] / 2.55, - }); + return ( + this.colorLib.rgbtoSColor({ + R: textureData[colorIndex] / 2.55, + G: textureData[colorIndex + 1] / 2.55, + B: textureData[colorIndex + 2] / 2.55, + }) || "0" + ); } return this.colorLib.rgbtoSColor({ R: 100, G: 100, B: 100 }); } @@ -4162,7 +4158,7 @@ curCostume.height ); if (textureData) { - return textureData; + return textureData || ""; } return ""; } @@ -4341,8 +4337,6 @@ // prettier-ignore if (!this.inDrawRegion) renderer.enterDrawRegion(this.penPlusDrawRegion); - gl.viewport(0, 0, nativeSize[0], nativeSize[1]); - //Safe to assume they have a buffer; const buffer = this.programs[shader].buffer; @@ -6041,7 +6035,7 @@ //Ignore reductive values if (!(id > 0 && id <= 3)) return def; - if (!value) return def; + if (typeof value == "undefined") return def; //Parse it let parsed = JSON.parse(def); diff --git a/extensions/rixxyx.js b/extensions/rixxyx.js index df67b10712..a8a4799e0f 100644 --- a/extensions/rixxyx.js +++ b/extensions/rixxyx.js @@ -9,13 +9,19 @@ * This file is available under an informal "use with credit" license. */ -(function () { +(function (Scratch) { "use strict"; var count = 0; var isMeasure = false; var time = 0; + Scratch.vm.runtime.on("AFTER_EXECUTE", () => { + if (isMeasure) { + time += 1; + } + }); + class RixxyX { getInfo() { return { @@ -480,11 +486,8 @@ isMeasure = false; } returnTime(args) { - if (isMeasure == true) { - time += 1; - } return time; } } Scratch.extensions.register(new RixxyX()); -})(); +})(Scratch); diff --git a/extensions/text.js b/extensions/text.js index e03c7705a9..a3195e0fea 100644 --- a/extensions/text.js +++ b/extensions/text.js @@ -2,6 +2,7 @@ // ID: strings // Description: Manipulate characters and text. // Original: CST1229 +// By: BludIsAnLemon // License: MIT AND MPL-2.0 (function (Scratch) { @@ -372,12 +373,95 @@ }, }, }, + + "---", + { + opcode: "posWith", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("[STRING] [POSITION]s with [SUBSTRING]?"), + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: "turbowarp", + }, + POSITION: { + type: Scratch.ArgumentType.STRING, + menu: "positions", + }, + SUBSTRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: "turbo", + }, + }, + }, + + "---", + + { + opcode: "reverse", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("reverse text [STRING]"), + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("apple"), + }, + }, + }, + + "---", + + { + opcode: "trim", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("trim whitespace [STRING] from [METHOD]"), + arguments: { + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: ` ${Scratch.translate("apple")} `, + }, + METHOD: { + type: Scratch.ArgumentType.STRING, + menu: "trimMethod", + }, + }, + }, ], menus: { textCase: { acceptReporters: true, items: this._initCaseMenu(), }, + positions: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("start"), + value: "starts", + }, + { + text: Scratch.translate("end"), + value: "ends", + }, + ], + }, + trimMethod: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("both sides"), + value: "both", + }, + { + text: Scratch.translate("the end"), + value: "end", + }, + { + text: Scratch.translate("the start"), + value: "start", + }, + ], + }, }, }; } @@ -613,6 +697,29 @@ return string; } } + posWith(args) { + const STRING = args.STRING.toString(); + const SUBSTRING = args.SUBSTRING.toString(); + if (args.POSITION.toString() === "starts") { + return STRING.startsWith(SUBSTRING); + } + return STRING.endsWith(SUBSTRING); + } + reverse(args) { + return Array.from(args.STRING.toString()).reverse().join(""); + } + trim(args) { + const STRING = args.STRING.toString(); + switch (args.METHOD.toString()) { + case "start": + return STRING.trimStart(); + case "end": + return STRING.trimEnd(); + case "both": + default: + return STRING.trim(); + } + } } Scratch.extensions.register(new StringsExt()); diff --git a/extensions/veggiecan/mobilekeyboard.js b/extensions/veggiecan/mobilekeyboard.js index 2ddf765fd6..86f0ec307d 100644 --- a/extensions/veggiecan/mobilekeyboard.js +++ b/extensions/veggiecan/mobilekeyboard.js @@ -19,7 +19,8 @@ class MobileKeyboard { constructor() { this.keyboardOpen = false; - this.waitCallback = null; + /** @type {Array<() => void>} */ + this.waitCallbacks = []; this.defaultValue = ""; this.inputElement = null; } @@ -34,6 +35,10 @@ blockIconURI: blockicon, name: Scratch.translate("Mobile Keyboard"), blocks: [ + { + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate("Currently only works on Android"), + }, { opcode: "showKeyboardBlock", blockType: Scratch.BlockType.COMMAND, @@ -207,10 +212,10 @@ input.parentNode.removeChild(input); } - if (this.waitCallback) { - this.waitCallback(); - this.waitCallback = null; + for (const callback of this.waitCallbacks) { + callback(); } + this.waitCallbacks.length = 0; }; input.addEventListener("input", () => { @@ -236,7 +241,7 @@ showKeyboardAndWaitBlock(args) { return new Promise((resolve) => { - this.waitCallback = resolve; + this.waitCallbacks.push(() => resolve()); this.showKeyboard(args.TYPE); }); } diff --git a/package-lock.json b/package-lock.json index 921fb49bbb..595b11f2ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,17 +13,18 @@ "@turbowarp/scratchblocks": "^3.6.5", "@turbowarp/types": "git+https://github.com/TurboWarp/types-tw.git#tw", "adm-zip": "^0.5.16", - "chokidar": "^4.0.1", + "chokidar": "^4.0.3", "ejs": "^3.1.10", - "express": "^4.21.1", - "image-size": "^1.1.1", + "express": "^4.21.2", + "image-size": "^1.2.0", "markdown-it": "^14.1.0" }, "devDependencies": { - "eslint": "^9.15.0", + "@transifex/api": "^7.1.3", + "eslint": "^9.17.0", "espree": "^9.6.1", "esquery": "^1.6.0", - "prettier": "^3.3.3", + "prettier": "^3.4.2", "spdx-expression-parse": "^4.0.0" } }, @@ -136,9 +137,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -226,6 +227,19 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@transifex/api": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@transifex/api/-/api-7.1.3.tgz", + "integrity": "sha512-oT6QXhRqduGqDNMfyRK4+u1RrAux0sCJe7rtFvEDc6oDg8h6dIr+2O2o8oSOio6E+JFS9KVrRbCIy2jFsPtRFQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "core-js": "^3.35.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@turbowarp/json": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@turbowarp/json/-/json-0.1.2.tgz", @@ -238,8 +252,8 @@ }, "node_modules/@turbowarp/types": { "version": "0.0.12", - "resolved": "git+ssh://git@github.com/TurboWarp/types-tw.git#1f858811919809efa4dbd789d27f8e513a32ee75", - "integrity": "sha512-OsBAn//X6giPsnBQ2ycA8ZRlMBXhnrWo89/qe5gWEaT0oHSqEN+leGNQJylR28UdbNddSTTjkcnAZ0cptHwKnA==", + "resolved": "git+ssh://git@github.com/TurboWarp/types-tw.git#f90c2495b65f1d64f6013840ef135e3039060c95", + "integrity": "sha512-iRX8dFvmunsDKhbRKwX2WNXVpa6miBUJ5ynFp40dLqNo01RKK24QvgHKHIDd7s7cLooeglPgP2PmrNdvvRWUdQ==", "license": "Apache-2.0" }, "node_modules/@types/estree": { @@ -441,10 +455,9 @@ } }, "node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", - "license": "MIT", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dependencies": { "readdirp": "^4.0.1" }, @@ -508,10 +521,22 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-js": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", + "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", - "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -653,9 +678,9 @@ } }, "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -663,7 +688,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", + "@eslint/js": "9.17.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -672,7 +697,7 @@ "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -848,9 +873,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -871,7 +896,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -886,6 +911,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -1176,9 +1205,9 @@ } }, "node_modules/image-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", - "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz", + "integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==", "dependencies": { "queue": "6.0.2" }, @@ -1565,9 +1594,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -1579,9 +1608,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/package.json b/package.json index 07dcb94916..1a84b5bd41 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "@turbowarp/extensions", "version": "0.0.1", "description": "Unsandboxed extensions for TurboWarp", + "exports": { + "./builder": "./development/builder.js" + }, "scripts": { "start": "node development/server.js", "dev": "node development/server.js", @@ -9,7 +12,9 @@ "validate": "node development/validate.js", "lint": "eslint development extensions --max-warnings=0", "format": "prettier . --write", - "check-format": "prettier . --check" + "check-format": "prettier . --check", + "upload-translations": "node development/upload-translations.js", + "download-translations": "node development/download-translations.js" }, "repository": { "type": "git", @@ -25,17 +30,18 @@ "@turbowarp/scratchblocks": "^3.6.5", "@turbowarp/types": "git+https://github.com/TurboWarp/types-tw.git#tw", "adm-zip": "^0.5.16", - "chokidar": "^4.0.1", + "chokidar": "^4.0.3", "ejs": "^3.1.10", - "express": "^4.21.1", - "image-size": "^1.1.1", + "express": "^4.21.2", + "image-size": "^1.2.0", "markdown-it": "^14.1.0" }, "devDependencies": { - "eslint": "^9.15.0", + "@transifex/api": "^7.1.3", + "eslint": "^9.17.0", "espree": "^9.6.1", "esquery": "^1.6.0", - "prettier": "^3.3.3", + "prettier": "^3.4.2", "spdx-expression-parse": "^4.0.0" }, "private": true diff --git a/translations/extension-metadata.json b/translations/extension-metadata.json index 4ed3a27fa0..d15da02d21 100644 --- a/translations/extension-metadata.json +++ b/translations/extension-metadata.json @@ -494,6 +494,7 @@ "obviousAlexC/newgroundsIO@description": "Blocchi che permettono ai giochi di interagire con l'API Newgrounds API. Non ufficiale.", "obviousAlexC/penPlus@description": "Capacità di rendering avanzate.", "obviousAlexC/penPlus@name": "Penna+ V7", + "penplus@description": "Rimpiazzata da Penna Plus V7.", "penplus@name": "Penna Plus V5 (Vecchio)", "pointerlock@description": "Aggiunge blocchi per bloccare il mouse. I blocchi\" x/y del mouse\" restituiscono di quanto è cambiata la posizione rispetto al frame precedente mentre il puntatore è bloccato. Rimpiazza il \"blocco puntatore\" sperimentale.", "pointerlock@name": "Blocco Puntatore", @@ -508,6 +509,7 @@ "shreder95ua/resolution@name": "Risoluzione Schermo", "sound@description": "Riproduce suoni dai loro URL.", "sound@name": "Suoni", + "steamworks@description": "Collega il tuo progetto all'API di Steamworks.", "stretch@description": "Stira gli sprite in orizzontale e in verticale.", "stretch@name": "Stira", "text@description": "Manipola caratteri e testi.", @@ -722,7 +724,7 @@ "CST1229/images@description": "이미지를 다루는 블록들입니다.", "CST1229/images@name": "이미지", "CST1229/zip@description": ".zip 또는 .sb3 형식의 파일을 생성하고 수정합니다.", - "Clay/htmlEncode@description": "신뢰하지 않는 텍스트를 HTML에 안전하게 포함할 수 있도록 이스케이프화 합니다.", + "Clay/htmlEncode@description": "신뢰하지 않는 텍스트를 HTML에 안전하게 포함할 수 있도록 이스케이프 처리합니다.", "Clay/htmlEncode@name": "HTML 인코딩", "CubesterYT/KeySimulation@description": "키 입력과 마우스 입력을 시뮬레이션 합니다.", "CubesterYT/KeySimulation@name": "키 시뮬레이션", @@ -835,7 +837,8 @@ "godslayerakp/ws@description": "WebSocket 서버에 직접적으로 연결합니다.", "iframe@description": "무대 위에 웹페이지 또는 HTML을 띄웁니다.", "itchio@description": "itch.io 웹사이트와 상호작용하는 블록들입니다. 비공식.", - "lab/text@description": "텍스트를 간편하게 나타내고 움직입니다. Scratch Lab의 Animated Text 실험판과 호환됩니다.", + "lab/text@description": "텍스트를 간편하게 나타내고 애니메이션을 적용합니다. Scratch Lab의 Animated Text 실험판과 호환됩니다.", + "lab/text@name": "애니메이션 텍스트", "local-storage@description": "일시적으로 데이터를 저장합니다. 쿠키와 유사하지만, 보다 향상되었습니다.", "local-storage@name": "로컬 스토리지", "mbw/xml@description": "XML을 통해 값을 생성하거나 추출합니다.", @@ -1423,7 +1426,9 @@ "vercte/dictionaries@name": "Словари" }, "sl": { - "runtime-options@name": "Možnosti izvajanja" + "files@name": "Datoteke", + "runtime-options@name": "Možnosti izvajanja", + "text@name": "Besedilo" }, "sv": { "runtime-options@name": "Körtidsalternativ" diff --git a/translations/extension-runtime.json b/translations/extension-runtime.json index 873d7bde01..f5810696cd 100644 --- a/translations/extension-runtime.json +++ b/translations/extension-runtime.json @@ -1318,10 +1318,8 @@ "godslayerakp/http@_Response": "Vastaus", "godslayerakp/http@_Show Extra": "Näytä lisälohkot", "godslayerakp/http@_[name] from header": "[name] otsakkeesta", - "godslayerakp/http@_[name] in request form": "pyyntölomakkeen [name]", "godslayerakp/http@_[path] in request options": "pyyntöasetusten [path]", "godslayerakp/http@_clear current data": "tyhjennä nykyiset tiedot", - "godslayerakp/http@_delete [name] from request form": "poista [name] pyyntölomakkeesta", "godslayerakp/http@_error": "virhe", "godslayerakp/http@_headers as json": "otsakkeet JSON-muodossa", "godslayerakp/http@_in header set [name] to [value]": "aseta otsakkeen [name] arvoon [value]", @@ -1329,13 +1327,11 @@ "godslayerakp/http@_request succeeded?": "onnistuiko pyyntö?", "godslayerakp/http@_response": "vastaus", "godslayerakp/http@_send request to [url]": "lähetä pyyntö osoitteeseen [url]", - "godslayerakp/http@_set [name] to [value] in request form": "aseta pyyntölomakkeen [name] arvoon [value]", "godslayerakp/http@_set [path] to [value] in request options": "aseta pyyntöasetusten [path] arvoon [value]", "godslayerakp/http@_set [path] to type [type] in request options": "aseta pyyntöasetusten kohteen [path] tyypiksi [type]", "godslayerakp/http@_set content type to [type]": "aseta sisällön tyypiksi [type]", "godslayerakp/http@_set headers to json [json]": "aseta otsakkeet JSON-koodiksi [json]", "godslayerakp/http@_set request body to [text]": "aseta pyynnön rungoksi [text]", - "godslayerakp/http@_set request body to a form": "aseta pyynnön runko lomakkeeksi", "godslayerakp/http@_set request method to [method]": "aseta pyyntömenetelmäksi [method]", "godslayerakp/http@_site responded?": "vastasiko sivusto?", "godslayerakp/http@_status": "tila", @@ -1512,9 +1508,6 @@ "obviousAlexC/SensingPlus@_No sprites exist": "Ei hahmoja", "obviousAlexC/SensingPlus@_Sensing+": "Tuntoaisti +", "obviousAlexC/SensingPlus@_Speech recording is unreliable": "Puheen äänitys on epäluotettava", - "obviousAlexC/SensingPlus@_Touch blocks are broken in Safari.": "Kosketuslohkot eivät toimi Safarissa.", - "obviousAlexC/SensingPlus@_We will try to fix them soon.": "Pyrimme korjaamaan ne pian.", - "obviousAlexC/SensingPlus@_[type] speed on the [axis] axis": "[type] nopeus [axis]-akselilla", "obviousAlexC/SensingPlus@_accelerometer": "kiihtyvyysmittari", "obviousAlexC/SensingPlus@_brightness": "kirkkaus", "obviousAlexC/SensingPlus@_color": "väri", @@ -1537,11 +1530,9 @@ "obviousAlexC/SensingPlus@_off": "pois päältä", "obviousAlexC/SensingPlus@_on": "päälle", "obviousAlexC/SensingPlus@_pixelate": "pikselöi", - "obviousAlexC/SensingPlus@_positional": "paikallinen", "obviousAlexC/SensingPlus@_recognized Words": "tunnistetut sanat", "obviousAlexC/SensingPlus@_recording?": "onko äänitys päällä?", "obviousAlexC/SensingPlus@_rotation style": "kiertotyyli", - "obviousAlexC/SensingPlus@_rotational": "kierto", "obviousAlexC/SensingPlus@_set clipboard to [TEXT]": "aseta leikepöydän sisällöksi [TEXT]", "obviousAlexC/SensingPlus@_sprite layer": "hahmon taso", "obviousAlexC/SensingPlus@_supports touches?": "tuetaanko kosketusta?", @@ -1843,9 +1834,11 @@ "text@_Title Case": "Alkukirjaimet Isolla", "text@_UPPERCASE": "ISOT KIRJAIMET", "text@_[STRING] matches regex /[REGEX]/[FLAGS]?": "vastaako [STRING] säännöllistä lauseketta /[REGEX]/[FLAGS]?", + "text@_apple": "omena", "text@_convert [STRING] to [TEXTCASE]": "muunna [STRING] muotoon [TEXTCASE]", "text@_count [SUBSTRING] in [STRING]": "merkkien [SUBSTRING] määrä merkkijonossa [STRING]", "text@_count regex /[REGEX]/[FLAGS] in [STRING]": "laske säännöllinen lauseke /[REGEX]/[FLAGS] merkkijonossa [STRING]", + "text@_end": "loppunäppäintä", "text@_index of [SUBSTRING] in [STRING]": "merkkijonon [SUBSTRING] järjestysnumero merkkijonossa [STRING]", "text@_is [OPERAND1] identical to [OPERAND2]?": "onko [OPERAND1] täysin sama kuin [OPERAND2]?", "text@_is [STRING] [TEXTCASE]?": "onko [STRING] muodossa [TEXTCASE]?", @@ -2657,6 +2650,8 @@ "stretch@_x stretch": "deformazione x", "stretch@_y stretch": "deformazione y", "text@_Text": "Testo", + "text@_apple": "mela", + "text@_end": "fine", "true-fantom/base@_Base": "Basi", "true-fantom/couplers@_Couplers": "Adattatori", "true-fantom/math@_Math": "Matematica", @@ -2960,11 +2955,31 @@ "Lily/Cast@_string": "文字列", "Lily/Cast@_type of [INPUT]": "[INPUT]の型", "Lily/ClonesPlus@_Clones+": "クローン +", + "Lily/ClonesPlus@_[INPUTA] of clone with [INPUTB] set to [INPUTC]": "変数[INPUTB]が[INPUTC]にされたクローンの[INPUTA]", + "Lily/ClonesPlus@_[INPUT] of main sprite": "メインスプライトの[INPUT]", "Lily/ClonesPlus@_clone count": "クローン数", + "Lily/ClonesPlus@_clone count of [INPUT]": "[INPUT]のクローン数", + "Lily/ClonesPlus@_clone with [INPUTA] set to [INPUTB] exists?": "変数[INPUTA]が[INPUTB]にされたクローンが存在する", + "Lily/ClonesPlus@_costume #": "コスチュームの番号", + "Lily/ClonesPlus@_costume name": "コスチュームの名前", + "Lily/ClonesPlus@_create clone with [INPUTA] set to [INPUTB]": "変数[INPUTA]を[INPUTB]にしてクローンを作る", + "Lily/ClonesPlus@_delete clones in [INPUT]": "[INPUT]のクローンを削除する", + "Lily/ClonesPlus@_delete clones with [INPUTA] set to [INPUTB]": "変数[INPUTA]が[INPUTB]にされたクローンを削除する", "Lily/ClonesPlus@_direction": "方向", "Lily/ClonesPlus@_is clone?": "クローン", + "Lily/ClonesPlus@_myself": "自分自身", + "Lily/ClonesPlus@_set variable [INPUTA] to [INPUTB] for clones with [INPUTC] set to [INPUTD]": "変数[INPUTC]を[INPUTD]にされたクローンの変数[INPUTA]を[INPUTB]にする", + "Lily/ClonesPlus@_set variable [INPUTA] to [INPUTB] for main sprite": "メインスプライトの変数[INPUTA]を[INPUTB]にする", "Lily/ClonesPlus@_size": "サイズ", + "Lily/ClonesPlus@_stop scripts in [INPUT]": "[INPUT]のスクリプトを止める", + "Lily/ClonesPlus@_stop scripts in clones with [INPUTA] set to [INPUTB]": "変数[INPUTA]が[INPUTB]にされたクローンのスクリプトを止める", + "Lily/ClonesPlus@_stop scripts in main sprite": "メインスプライトのスクリプトを止める", + "Lily/ClonesPlus@_touching clone with [INPUTA] set to [INPUTB]?": "変数[INPUTA]が[INPUTB]になったクローンに触れた", + "Lily/ClonesPlus@_touching main sprite?": "メインのスプライトに触れた", + "Lily/ClonesPlus@_variable [INPUTA] of clone with [INPUTB] set to [INPUTC]": "変数[INPUTB]が[INPUTC]にされたクローンの変数[INPUTA]", + "Lily/ClonesPlus@_variable [INPUT] of main sprite": "メインスプライトの変数[INPUT]", "Lily/ClonesPlus@_volume": "音量", + "Lily/ClonesPlus@_when I start as a clone with [INPUTA] set to [INPUTB]": "変数[INPUTA]が[INPUTB]にされてクローンされたとき", "Lily/ClonesPlus@_x position": "X 位置", "Lily/ClonesPlus@_y position": "Y位置", "Lily/CommentBlocks@_Comment Blocks": "コメントブロック", @@ -2975,19 +2990,23 @@ "Lily/ListTools@_[LIST] as array": "[LIST]のarray", "Lily/ListTools@_[LIST] is empty?": "[LIST]が空", "Lily/ListTools@_ascending": "数が増加", + "Lily/ListTools@_concatenate [LIST1] onto [LIST2]": "[LIST1]の中身を[LIST2]に追加する", "Lily/ListTools@_delete all [ITEM] in [LIST]": "[LIST]の[ITEM]をすべて削除する", "Lily/ListTools@_delete items [NUM1] to [NUM2] of [LIST]": "[LIST]の[NUM1]番から[NUM2]番までを削除する", "Lily/ListTools@_descending": "数が減少", "Lily/ListTools@_first": "最初の", + "Lily/ListTools@_index # [INDEX] of item [ITEM] in [LIST]": "[LIST]の[ITEM]が[INDEX]番目に出てきた場所", "Lily/ListTools@_item [NUM] exists in [LIST]?": "[LIST]に[NUM]番目が存在する", "Lily/ListTools@_last": "最後の", "Lily/ListTools@_list [LIST] joined by [STRING]": "[STRING]でつなげた[LIST]", "Lily/ListTools@_order of [LIST] is [ORDER]?": "[LIST]が[ORDER]", "Lily/ListTools@_random": "ランダム", "Lily/ListTools@_randomized": "ランダム", + "Lily/ListTools@_repeat [LIST1] [NUM] times in [LIST2]": "[LIST1]を[LIST2]で[NUM]回繰り返す", "Lily/ListTools@_replace all [ITEM1] with [ITEM2] in [LIST]": "[LIST]のすべての[ITEM1]を[ITEM2]で置き換える", "Lily/ListTools@_reversed": "順番が逆", "Lily/ListTools@_set [LIST] to array [ARRAY]": "[LIST]をarray[ARRAY]にセットする", + "Lily/ListTools@_set items of [LIST1] to [LIST2]": "[LIST1]の中身を[LIST2]にする", "Lily/ListTools@_set order of [LIST] to [ORDER]": "[LIST]を[ORDER]にする", "Lily/LooksPlus@_# of costumes in [TARGET]": "[TARGET]のコスチュームの数", "Lily/LooksPlus@_Looks+": "見た目 +", @@ -3019,8 +3038,10 @@ "Lily/LooksPlus@_whirl": "渦巻き", "Lily/LooksPlus@_width": "横幅", "Lily/McUtils@_broken": "壊れて", - "Lily/McUtils@_if [INPUTA] is manager then [INPUTB] else [INPUTC]": "[INPUTA]はマネージャーであるなら[INPUTB]、でなげれば[INPUTC]", + "Lily/McUtils@_if [INPUTA] is manager then [INPUTB] else [INPUTC]": "[INPUTA]はマネージャーであるなら[INPUTB]、でなければ[INPUTC]", "Lily/McUtils@_is ice cream machine [INPUT]": "アイスクリーム機は[INPUT]いますか?", + "Lily/McUtils@_place order [INPUT]": "[INPUT]を注文する", + "Lily/McUtils@_talk to manager [INPUT]": "マネージャーに[INPUT]話しかける", "Lily/McUtils@_working": "動いて", "Lily/MoreEvents@_More Events": "その他のイベント", "Lily/MoreEvents@_after project saves": "プロジェクトが保存されたあと", @@ -3122,6 +3143,7 @@ "Lily/Video@_loaded videos": "読み込まれた動画", "Lily/Video@_pause video [NAME]": "動画[NAME]を一時停止する", "Lily/Video@_paused": "一時停止している", + "Lily/Video@_playback rate": "再生速度", "Lily/Video@_playing": "再生している", "Lily/Video@_resume video [NAME]": "動画[NAME]を再開する", "Lily/Video@_set volume of video [NAME] to [VALUE]": "動画[NAME]の音量を[VALUE]にセットする", @@ -3136,7 +3158,10 @@ "Lily/lmsutils@_Lily's Toolbox": "Lilyの道具箱", "Lily/lmsutils@_Show Legacy Blocks": "古いブロックを表示する", "Lily/lmsutils@_[DROPDOWN] of user": "ユーザーの[DROPDOWN]", + "Lily/lmsutils@_[INPUTA] nand [INPUTB]": "[INPUTA]かつ[INPUTB]ではない", "Lily/lmsutils@_[INPUTA] nor [INPUTB]": "[INPUTA]と[INPUTB]のどちらでもない", + "Lily/lmsutils@_[INPUTA] xnor [INPUTB]": "[INPUTA]と[INPUTB]が両方真か両方偽", + "Lily/lmsutils@_[INPUTA] xor [INPUTB]": "[INPUTA]と[INPUTB]の片方が真", "Lily/lmsutils@_[INPUT] is [DROPDOWN]": "[INPUT]が[DROPDOWN]", "Lily/lmsutils@_[STRING] to lowercase": "[STRING]を小文字にする", "Lily/lmsutils@_[STRING] to uppercase": "[STRING]を大文字にする", @@ -3146,12 +3171,14 @@ "Lily/lmsutils@_brightness": "明るさ", "Lily/lmsutils@_browser": "ブラウザ", "Lily/lmsutils@_change variable [INPUTA] by [INPUTB]": "変数[INPUTA]を[INPUTB]ずつ変える", + "Lily/lmsutils@_clamp [INPUTA] between [INPUTB] and [INPUTC]": "[INPUTA]を[INPUTB]から[INPUTC]までで表す", "Lily/lmsutils@_clear console": "コンソールをクリア", "Lily/lmsutils@_clipboard": "クリップボードの内容", "Lily/lmsutils@_clone count": "クローン数", "Lily/lmsutils@_color": "色", "Lily/lmsutils@_color [COLOUR]": "[COLOUR]のカラーコード", "Lily/lmsutils@_confirm [STRING]": "[STRING]の確認", + "Lily/lmsutils@_console [DROPDOWN] [INPUT]": "コンソールに[DROPDOWN]として[INPUT]を記入する", "Lily/lmsutils@_decode [STRING] from [DROPDOWN]": "[STRING]を[DROPDOWN]からデコード", "Lily/lmsutils@_delete all variables": "すべての変数を削除する", "Lily/lmsutils@_delete variable [INPUT]": "変数[INPUT]を削除する", @@ -3172,8 +3199,11 @@ "Lily/lmsutils@_letters [INPUTA] to [INPUTB] of [STRING]": "[STRING]の[INPUTA]から[INPUTB]番目", "Lily/lmsutils@_list active variables": "アクティブな変数", "Lily/lmsutils@_lowercase": "小文字", + "Lily/lmsutils@_matrix [MATRIX]": "[MATRIX]の行列", "Lily/lmsutils@_mosaic": "モザイク", "Lily/lmsutils@_newline character": "改行文字", + "Lily/lmsutils@_normalise [INPUT]": "[INPUT]を標準化する", + "Lily/lmsutils@_note [NOTE]": "[NOTE]の音符", "Lily/lmsutils@_number": "数字", "Lily/lmsutils@_open link [INPUT] in new tab": "リンク[INPUT]を新しいタブで開く", "Lily/lmsutils@_operating system": "オペレーションシステム", @@ -3182,6 +3212,7 @@ "Lily/lmsutils@_prompt [STRING]": "[STRING]のプロンプト", "Lily/lmsutils@_random": "ランダム", "Lily/lmsutils@_redirect to link [INPUT]": "リンク[INPUT]にリダイレクトする", + "Lily/lmsutils@_replace SVG data for costume [INPUTA] with [INPUTB]": "コスチュームの[INPUTA]番目をSVGデータ[INPUTB]で置き換える", "Lily/lmsutils@_replace first [INPUTA] with [INPUTB] in [STRING]": "[STRING]の[INPUTA]を[INPUTB]で置き換える", "Lily/lmsutils@_reverse [STRING]": "[STRING]を逆から読む", "Lily/lmsutils@_screen [DROPDOWN]": "画面の[DROPDOWN]", @@ -3202,6 +3233,7 @@ "Longboost/color_channels@_[COLOR] channel enabled?": "[COLOR]チャンネルが有効", "Longboost/color_channels@_blue": "青", "Longboost/color_channels@_clear color draw effects": "色描画エフェクトを無効にする", + "Longboost/color_channels@_enable depth mask? [DRAW]": "デプスマスクが有効[DRAW]", "Longboost/color_channels@_false": "偽", "Longboost/color_channels@_green": "緑", "Longboost/color_channels@_off": "オフ", @@ -3211,8 +3243,19 @@ "Longboost/color_channels@_red": "赤", "Longboost/color_channels@_set colors red:[R] green:[G] blue:[B]": "色を赤:[R]緑:[G]青:[B]にする", "Longboost/color_channels@_true": "真", + "NOname-awa/graphics2d@area": "面積", + "NOname-awa/graphics2d@circumference": "円周", + "NOname-awa/graphics2d@diameter": "直径", + "NOname-awa/graphics2d@graph": "グラフ[graph]の[CS]", + "NOname-awa/graphics2d@line_section": "([x1],[y1]) から ([x2],[y2]) までの長さ", "NOname-awa/graphics2d@name": "グラフィック2D", "NOname-awa/graphics2d@pi": "π", + "NOname-awa/graphics2d@quadrilateral": "([x1],[y1]) ([x2],[y2]) ([x3],[y3]) ([x4],[y4]) の四角形の[CS] ", + "NOname-awa/graphics2d@radius": "半径", + "NOname-awa/graphics2d@ray_direction": "([x1],[y1]) から ([x2],[y2]) への向き", + "NOname-awa/graphics2d@round": "[rd]が[a]の円の[CS]", + "NOname-awa/graphics2d@triangle": "([x1],[y1]) ([x2],[y2]) ([x3],[y3]) の三角形の[CS]", + "NOname-awa/graphics2d@triangle_s": "[s1][s2][s3]の三角形の面積", "NOname-awa/more-comparisons@_More Comparisons": "さらなる比較", "NOname-awa/more-comparisons@_false": "偽", "NOname-awa/more-comparisons@_true": "真", @@ -3239,9 +3282,11 @@ "NexusKitten/moremotion@_point towards x: [X] y: [Y]": "x座標[X]、y座標[Y]の位置を向く", "NexusKitten/moremotion@_rotation style": "回転方法", "NexusKitten/moremotion@_sprite [WHAT]": "スプライトの[WHAT]", + "NexusKitten/moremotion@_touching rectangle x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?": "四角形 x1[X1]y1[Y1]x2[X2]y2[Y2]に触れた", "NexusKitten/moremotion@_touching x: [X] y: [Y]?": "x座標[X]y座標[Y]に触れた", "NexusKitten/moremotion@_width": "横幅", "NexusKitten/sgrab@_S-Grab": "S-グラブ", + "NexusKitten/sgrab@_[WHAT] of user [WHO]": "ユーザー[WHO]の[WHAT]", "NexusKitten/sgrab@_about me": "私について", "NexusKitten/sgrab@_creator of project id [WHO]": "プロジェクトID[WHO]の作者", "NexusKitten/sgrab@_favorite": "お気に入り", @@ -3280,12 +3325,54 @@ "SharkPool/Font-Manager@added_field_removed": "消された", "SharkPool/Font-Manager@added_input_added": "追加された", "SharkPool/Font-Manager@added_input_removed": "消された", + "Skyhigh173/bigint@_Arithmetic": "計算", "Skyhigh173/bigint@_BigInt": "ビッグイント", "Skyhigh173/bigint@_Bitwise": "ビット操作", + "Skyhigh173/bigint@_Logic": "論理", + "Skyhigh173/bigint@_[a] mod [b]": "[a]を[b]で割った余り", + "Skyhigh173/bigint@_convert BigInt [text] to number": "ビッグイント[text]を数字にする", + "Skyhigh173/bigint@_convert number [text] to BigInt": "数字[text]をビッグイントにする", "Skyhigh173/json@_Advanced": "詳細設定", "Skyhigh173/json@_General Utils": "汎用ユーティリティ", + "Skyhigh173/json@_Lists": "リスト", + "Skyhigh173/json@_Object": "オブジェクト", + "Skyhigh173/json@_[json1] [equal] [json2]": "[json1][equal][json2]", + "Skyhigh173/json@_[json] contains key [key]?": "[json]がキー[key]を含む", + "Skyhigh173/json@_[json] contains value [value]?": "[json]が値[value]を含む", + "Skyhigh173/json@_add [item] to array [json]": "[item]をArray[json]に追加する", + "Skyhigh173/json@_all [Stype] of [json]": "[json]のすべての[Stype]", + "Skyhigh173/json@_array concat [json] [json2]": "Array[json]と[json2]をつなぐ", + "Skyhigh173/json@_array from text [json]": "テキスト[json]からのArray", "Skyhigh173/json@_ascending": "数が増えていく", + "Skyhigh173/json@_create array by [text] with delimiter [d]": "Arrayを[text]から区切り文字[d]で作る", + "Skyhigh173/json@_datas": "データ", + "Skyhigh173/json@_delete [item] in [json]": "[json]の[item]を削除する", + "Skyhigh173/json@_delete all [item] in array [json]": "Array[json]の[item]をすべて削除する", + "Skyhigh173/json@_delete item [item] of array [json]": "Array[json]の[item]番目を削除する", "Skyhigh173/json@_descending": "数が減少", + "Skyhigh173/json@_flat array [json] by depth [depth]": "Array[json]を深さ[depth]で平らにする", + "Skyhigh173/json@_get all values with key [key] in array [json]": "キー[key]のすべての値をArray[json]から取得する", + "Skyhigh173/json@_get list [list] as array": "リスト[list]のArray", + "Skyhigh173/json@_insert [item] at [pos] of array [json]": "Array[json]の[pos]番目に[item]を挿入する", + "Skyhigh173/json@_is JSON [json] valid?": "JSON[json]が有効", + "Skyhigh173/json@_is [json] [types]?": "[json]が[types]", + "Skyhigh173/json@_item # of [item] in array [json]": "Array[json]の[item]の場所", + "Skyhigh173/json@_item [item] of array [json]": "Array[json]の[item]番目", + "Skyhigh173/json@_items [item] to [item2] of array [json]": "Array[json]の[item]から[item2]番目", + "Skyhigh173/json@_join string by array [json] with delimiter [d]": "Array[json]を区切り文字[d]でつなげる", + "Skyhigh173/json@_keys": "キー", + "Skyhigh173/json@_length of array [json]": "Array[json]の長さ", + "Skyhigh173/json@_length of json [json]": "JSON[json]の長さ", + "Skyhigh173/json@_new [json]": "新しい[json]", + "Skyhigh173/json@_replace item [pos] of [json] with [item]": "[json]の[pos]番目を[item]で置き換える", + "Skyhigh173/json@_reverse array [json]": "Array[json]の順番を逆にする", + "Skyhigh173/json@_select a list": "リストを選択", + "Skyhigh173/json@_set [item] in [json] to [value]": "[json]の[item]を[value]にする", + "Skyhigh173/json@_set length of array [json] to [len]": "Array[json]の長さを[len]にする", + "Skyhigh173/json@_set list [list] to [json]": "リスト[list]を[json]にする", + "Skyhigh173/json@_sort array [list] in [order] order": "Array[list]を[order]ように並べる", + "Skyhigh173/json@_value of [item] in [json]": "[json]の[item]の値", + "Skyhigh173/json@_values": "値", "TheShovel/CanvasEffects@_Canvas Effects": "キャンバス効果", "TheShovel/CanvasEffects@_background": "背景", "TheShovel/CanvasEffects@_blur": "ぼかし", @@ -3306,8 +3393,11 @@ "TheShovel/CanvasEffects@_inset": "埋め込み", "TheShovel/CanvasEffects@_invert": "色の反転", "TheShovel/CanvasEffects@_none": "無し", + "TheShovel/CanvasEffects@_offset X": "Xオフセット", + "TheShovel/CanvasEffects@_offset Y": "Yオフセット", "TheShovel/CanvasEffects@_outset": "出っ張り", "TheShovel/CanvasEffects@_pixelated": "ピクセル化", + "TheShovel/CanvasEffects@_resize rendering mode": "描画モード", "TheShovel/CanvasEffects@_ridge": "リッジ", "TheShovel/CanvasEffects@_rotation": "回転", "TheShovel/CanvasEffects@_saturation": "彩度", @@ -3336,8 +3426,16 @@ "TheShovel/CustomStyles@_Custom Styles": "カスタムスタイル", "TheShovel/CustomStyles@_disabled": "無効", "TheShovel/CustomStyles@_enabled": "有効", + "TheShovel/CustomStyles@_image [URL]": "画像[URL]", + "TheShovel/CustomStyles@_make a gradient with [COLOR1] and [COLOR2] at angle [ANGLE]": "グラデーションを[COLOR1]と[COLOR2]で角度[ANGLE]で作る", + "TheShovel/CustomStyles@_reset styles": "スタイルをリセット", "TheShovel/CustomStyles@_set [COLORABLE] to [COLOR]": "[COLORABLE]を[COLOR]にする", + "TheShovel/CustomStyles@_set ask prompt button image to [URL]": "聞いて待つプロンプトのボタン画像を[URL]にする", "TheShovel/CustomStyles@_set border width of [BORDER] to [SIZE]": "縁の幅[BORDER]を[SIZE]にする", + "TheShovel/CustomStyles@_set list scrolling to [SCROLLRULE]": "リストのスクロールを[SCROLLRULE]にする", + "TheShovel/CustomStyles@_set position of list [NAME] to x: [X] y: [Y]": "リスト[NAME]のいちをx[X]y[Y]にする", + "TheShovel/CustomStyles@_set position of variable [NAME] to x: [X] y: [Y]": "変数[NAME]の位置をx[X]y[Y]にする", + "TheShovel/CustomStyles@_set roundness of [CORNER] to [SIZE]": "[CORNER]の丸みを[SIZE]にする", "TheShovel/CustomStyles@_transparent": "透明", "TheShovel/LZ-String@_LZ Compress": "LZ圧縮", "TheShovel/LZ-String@_compress [TEXT] to [TYPE]": "[TEXT]を[TYPE]に圧縮", @@ -3404,11 +3502,25 @@ "clipboard@_when something is pasted": "何かが貼り付けられたとき", "clouddata-ping@_Ping Cloud Data": "クラウドデータのPing", "clouddata-ping@_is cloud data server [SERVER] up?": "クラウドサーバー[SERVER]が稼働している", + "cloudlink@_Apple": "りんご", + "cloudlink@_Banana": "バナナ", + "cloudlink@_Hide old blocks": "廃棄されたブロックを隠す", + "cloudlink@_Show old blocks": "廃棄されたブロックを表示する", + "cloudlink@_[PATH] of [JSON_STRING]": "[JSON_STRING]の[PATH]", + "cloudlink@_connected?": "接続している?", "cloudlink@_direct data": "ダイレクトデータ", + "cloudlink@_disconnect": "接続を切る", + "cloudlink@_extension version": "拡張機能のバージョン", + "cloudlink@_failed to connnect?": "接続が失敗した?", "cloudlink@_global data": "グローバルデータ", + "cloudlink@_link status": "リンクステータス", + "cloudlink@_linked to rooms?": "部屋に接続した?", + "cloudlink@_lost connection?": "接続を失った?", "cloudlink@_private data": "プライベートデータ", "cloudlink@_status code": "ステータスコード", + "cloudlink@_username synced?": "ユーザー名が同期された?", "cloudlink@_when connected": "接続されたとき", + "cloudlink@_when disconnected": "接続が切断されたとき", "cs2627883/numericalencoding@_Hello!": "こんにちは!", "cs2627883/numericalencoding@_Numerical Encoding V1": "数値エンコーディングV1", "cursor@_Mouse Cursor": "マウスカーソル", @@ -3441,18 +3553,22 @@ "files@_open a file as [as]": "[as]としてファイルを開く", "files@_text": "テキスト", "gamejolt@_Close": "閉じる", + "gamejolt@_auto login available?": "自動ログインを利用できますか?", "gamejolt@_comment": "コメント", "gamejolt@_day": "日", + "gamejolt@_fetch logged in user": "現在ログインしているユーザーを取ってくる", "gamejolt@_guest": "ゲスト", "gamejolt@_hour": "時", "gamejolt@_logged in user's username": "登録したユーザーの名前", "gamejolt@_logged in?": "登録している?", + "gamejolt@_login automatically": "自動的にログイン", "gamejolt@_logout": "ログアウト", "gamejolt@_minute": "分", "gamejolt@_month": "月", "gamejolt@_name": "名前", "gamejolt@_off": "オフ", "gamejolt@_on": "オン", + "gamejolt@_private token": "プライベートトークン", "gamejolt@_second": "秒", "gamejolt@_status": "ステータス", "gamejolt@_text": "テキスト", @@ -3462,7 +3578,13 @@ "gamejolt@_website": "ウェブサイト", "gamejolt@_year": "年", "gamepad@_Gamepad": "ゲームパッド", + "godslayerakp/http@_Request": "リクエスト", + "godslayerakp/http@_Response": "返答", + "godslayerakp/http@_clear current data": "現在のデータをクリアする", + "godslayerakp/http@_error": "エラー", + "godslayerakp/http@_headers as json": "ヘッダーをJSONにして", "godslayerakp/http@_status": "ステータス", + "godslayerakp/http@_status text": "ステータステキスト", "godslayerakp/ws@_close connection": "接続を切る", "godslayerakp/ws@_close connection with code [CODE]": "コード[CODE]で接続を切る", "godslayerakp/ws@_close connection with reason [REASON] and code [CODE]": "理由を[REASON]にしてコード[CODE]で接続を切る", @@ -3538,8 +3660,14 @@ "lab/text@disableCompatibilityMode": "これはScratch Labでは動かないブロックや機能を有効にします。\n\n続行しますか?", "local-storage@_Local Storage": "ローカルストレージ", "local-storage@_Local Storage extension: project must run the \"set storage namespace ID\" block before it can use other blocks": "ローカルストレージ拡張機能:他のブロックを実行する前に、「ストレージの名前を()にする」ブロックを実行する必要があります。", + "local-storage@_delete all keys": "すべてのキーを削除する", + "local-storage@_delete key [KEY]": "キー[KEY]を削除する", "local-storage@_get key [KEY]": "キーを取得[KEY]", + "local-storage@_project title": "プロンプトのタイトル", + "local-storage@_score": "スコア", + "local-storage@_set key [KEY] to [VALUE]": "キー[KEY]を[VALUE]にする", "local-storage@_set storage namespace ID to [ID]": "ストレージの名前空間IDを[ID]にする", + "local-storage@_when another window changes storage": "他のウィンドウがストレージを変えたとき", "mdwalters/notifications@_Hello, world!": "こんにちは、世界!", "mdwalters/notifications@_Notification from project": "プロジェクトからの通知", "mdwalters/notifications@_Notifications": "通知", @@ -3566,9 +3694,6 @@ "obviousAlexC/SensingPlus@_No sprites exist": "スプライトがありません", "obviousAlexC/SensingPlus@_Sensing+": "調べる+", "obviousAlexC/SensingPlus@_Speech recording is unreliable": "音声認識は精度がよくありません", - "obviousAlexC/SensingPlus@_Touch blocks are broken in Safari.": "タッチ系ブロックはSafariでは使えません。", - "obviousAlexC/SensingPlus@_We will try to fix them soon.": "私達はこれをもうすぐ治します。", - "obviousAlexC/SensingPlus@_[type] speed on the [axis] axis": "[axis]軸における[type]スピード", "obviousAlexC/SensingPlus@_accelerometer": "加速度センサー", "obviousAlexC/SensingPlus@_brightness": "明るさ", "obviousAlexC/SensingPlus@_color": "色", @@ -3591,11 +3716,9 @@ "obviousAlexC/SensingPlus@_off": "オフ", "obviousAlexC/SensingPlus@_on": "オン", "obviousAlexC/SensingPlus@_pixelate": "ピクセル化", - "obviousAlexC/SensingPlus@_positional": "座標的", "obviousAlexC/SensingPlus@_recognized Words": "認識された言葉", "obviousAlexC/SensingPlus@_recording?": "録音中", "obviousAlexC/SensingPlus@_rotation style": "回転方法", - "obviousAlexC/SensingPlus@_rotational": "回転的", "obviousAlexC/SensingPlus@_set clipboard to [TEXT]": "クリップボードに[TEXT]をセットする", "obviousAlexC/SensingPlus@_sprite layer": "今いるレイヤー", "obviousAlexC/SensingPlus@_supports touches?": "タッチ対応", @@ -3606,6 +3729,7 @@ "obviousAlexC/SensingPlus@_touching the original [Sprite]?": "元のスプライト[Sprite]に指が触れているか", "obviousAlexC/SensingPlus@_turn speech recording [toggle]": "音声認識を[toggle]にする", "obviousAlexC/SensingPlus@_whirl": "渦巻き", + "obviousAlexC/newgroundsIO@_score": "スコア", "obviousAlexC/newgroundsIO@_username": "ユーザー名", "obviousAlexC/penPlus@_Advanced": "詳細設定", "obviousAlexC/penPlus@_Color": "色", @@ -3618,6 +3742,7 @@ "obviousAlexC/penPlus@_Rotation": "回転", "obviousAlexC/penPlus@_Shader Editor": "シェーダーエディター", "obviousAlexC/penPlus@_Shader Manager": "シェーダーマネージャー", + "obviousAlexC/penPlus@_Triangle Blocks": "三角形ブロック", "obviousAlexC/penPlus@_Width": "横幅", "obviousAlexC/penPlus@_brightness": "明るさ", "obviousAlexC/penPlus@_color": "色", @@ -3629,7 +3754,9 @@ "obviousAlexC/penPlus@_on": "オン", "obviousAlexC/penPlus@_pen [HSV]": "ペンの[HSV]", "obviousAlexC/penPlus@_pen is down?": "ペンが下がった", + "obviousAlexC/penPlus@_reset triangle attributes": "三角形の属性をリセットする", "obviousAlexC/penPlus@_saturation": "彩度", + "obviousAlexC/penPlus@_shaders in project": "プロジェクトにあるシェーダー", "obviousAlexC/penPlus@_size": "サイズ", "obviousAlexC/penPlus@_stamp [sprite]": "[sprite]をスタンプ", "obviousAlexC/penPlus@_transparency": "透明度", @@ -3640,11 +3767,23 @@ "pointerlock@_enabled": "有効", "pointerlock@_pointer locked?": "ポインターはロックされている", "pointerlock@_set pointer lock [enabled]": "ポインターロックを[enabled]にする", + "qxsck/data-analysis@average": "[NUMBERS]の平均値", + "qxsck/data-analysis@maximum": "[NUMBERS]の最大値", + "qxsck/data-analysis@median": "[NUMBERS]の中央値", + "qxsck/data-analysis@minimum": "[NUMBERS]の最小値", "qxsck/data-analysis@name": "データ分析", + "qxsck/var-and-list@addValueInList": "[VALUE]を[LIST]に追加する", "qxsck/var-and-list@name": "変数とリスト", + "rixxyx@_RixxyX is cool, right?": "RixxyX はかっこいいではないか!", + "rixxyx@_[BOOL] as boolean": "[BOOL]をブーリアン値にして", + "rixxyx@_[NUM] as number": "[NUM]を数字にして", + "rixxyx@_[TEXT] to lowercase": "[TEXT]を小文字にする", + "rixxyx@_[TEXT] to uppercase": "[TEXT]を大文字にする", + "rixxyx@_binary [BIN] to text": "バイナリ[BIN]をテキストにして", "rixxyx@_false": "偽", "rixxyx@_if [BOOL] then [TEXT]": "もし[BOOL]なら[TEXT]", "rixxyx@_if [BOOL] then [TEXT_1] else [TEXT_2]": "もし[BOOL]なら[TEXT_1]、でなげれば[TEXT_2]", + "rixxyx@_rixxyX is cool, right?": "rixxyX はかっこいいではないか!", "rixxyx@_true": "真", "runtime-options@_Infinity": "無限", "runtime-options@_Runtime Options": "ランタイムのオプション", @@ -3658,6 +3797,8 @@ "runtime-options@_height": "高さ", "runtime-options@_high quality pen": "ペンできれいに描画する", "runtime-options@_interpolation": "補完機能", + "runtime-options@_remove fencing": "動く範囲と大きさの制限を解除する", + "runtime-options@_remove misc limits": "その他の制限を解除する", "runtime-options@_run green flag [flag]": "緑の旗[flag]を実行する", "runtime-options@_set [thing] to [enabled]": "[thing]を[enabled]にする", "runtime-options@_set clone limit to [limit]": "クローンの制限を[limit]にする", @@ -3665,16 +3806,52 @@ "runtime-options@_set stage size width: [width] height: [height]": "ステージの横幅を[width]高さを[height]にする", "runtime-options@_set username to [username]": "ユーザー名を[username]にする", "runtime-options@_stage [dimension]": "ステージの[dimension]", + "runtime-options@_stage size": "ステージのサイズ", "runtime-options@_turbo mode": "ターボモード", "runtime-options@_username": "ユーザー名", "runtime-options@_when [WHAT] changed": "[WHAT]が変更されたとき", "runtime-options@_width": "横幅", + "shreder95ua/resolution@_Screen resolution": "画面解像度", + "shreder95ua/resolution@_primary screen height": "画面の高さ", + "shreder95ua/resolution@_primary screen width": "画面の幅", "sound@_Sound": "音声", + "sound@_play sound from url: [path] until done": "終わるまでurl:[path]から音声を再生する", + "sound@_start sound from url: [path]": "url:[path]から音声を再生開始", + "steamworks@_DLC": "ダウンロードコンテンツ", + "steamworks@_IP country": "IPアドレスの割り当て国", "steamworks@_false": "偽", + "steamworks@_has steamworks?": "Steamworks は実装されていますか?", + "steamworks@_level": "レベル", "steamworks@_name": "名前", "steamworks@_true": "真", + "stretch@_Stretch": "伸縮", + "stretch@_change stretch by x: [DX] y: [DY]": "x座標の伸縮を[DX]、y座標の伸縮を[DY]ずつ変える", + "stretch@_change stretch x by [DX]": "x座標の伸縮を[DX]ずつ変える", + "stretch@_change stretch y by [DY]": "y座標の伸縮を[DY]ずつ変える", + "stretch@_set stretch to x: [X] y: [Y]": "x座標の伸縮を[X]、y座標の伸縮を[Y]にする", + "stretch@_set stretch x to [X]": "x座標の伸縮を[X]にする", + "stretch@_set stretch y to [Y]": "y座標の伸縮を[Y]にする", + "stretch@_x stretch": "x座標の伸縮", + "stretch@_y stretch": "y座標の伸縮", + "text@_Exactly Title Case": "正確なタイトルケース", + "text@_MiXeD CaSe": "ミックスケース", "text@_Text": "テキスト", + "text@_Title Case": "タイトルケース", + "text@_UPPERCASE": "大文字", + "text@_apple": "りんご", + "text@_convert [STRING] to [TEXTCASE]": "[STRING]を[TEXTCASE]に変換する", + "text@_count [SUBSTRING] in [STRING]": "[STRING]に[SUBSTRING]が出てきた回数", + "text@_end": "End", + "text@_index of [SUBSTRING] in [STRING]": "[STRING]の[SUBSTRING]の場所", + "text@_is [OPERAND1] identical to [OPERAND2]?": "[OPERAND1]と[OPERAND2]が同一", + "text@_is [STRING] [TEXTCASE]?": "[STRING]が[TEXTCASE]", + "text@_item [ITEM] of [STRING] split by [SPLIT]": "[STRING]を[SPLIT]で区切ったときの[ITEM]番目", + "text@_letters [LETTER1] to [LETTER2] of [STRING]": "[STRING]の[LETTER1]から[LETTER2]文字目", "text@_lowercase": "小文字", + "text@_repeat [STRING] [REPEAT] times": "[STRING]を[REPEAT]回繰り返す", + "text@_replace [SUBSTRING] in [STRING] with [REPLACE]": "[STRING]の[SUBSTRING]を[REPLACE]で置き換える", + "text@_unicode [NUM] as letter": "Unicode[NUM]の文字", + "text@_unicode of [STRING]": "[STRING]のUnicode", "true-fantom/base@_Base": "進数", "true-fantom/base@_[A] from base [B] to base [C]": "[B]進数の数[A]を[C]進数に変換する", "true-fantom/base@_is base [B] [A]?": "[A]は[B]進数で表現できる", @@ -3682,20 +3859,89 @@ "true-fantom/couplers@_angle [ANGLE]": "[ANGLE]度の角度", "true-fantom/couplers@_color [COLOUR]": "[COLOUR]のカラーコード", "true-fantom/couplers@_false": "偽", + "true-fantom/couplers@_matrix [MATRIX]": "[MATRIX]のマトリックス", + "true-fantom/couplers@_note [NOTE]": "[NOTE]の音符", "true-fantom/couplers@_true": "真", "true-fantom/math@_Math": "数学", + "true-fantom/math@_[A] exactly contains [B]?": "[A]が確かに[B]を含んでいる", + "true-fantom/math@_clamp [A] between [B] and [C]": "[A]を[B]から[C]までで表す", + "true-fantom/math@_is float [A]?": "[A]が小数", + "true-fantom/math@_is int [A]?": "[A]が整数", + "true-fantom/math@_is number [A]?": "[A]が数字", + "true-fantom/math@_is safe number [A]?": "[A]が安全な数字", + "true-fantom/math@_map [A] from range [m1] - [M1] to range [m2] - [M2]": "[A]の[m1]から[M1]までの範囲を[m2]から[M2]までの範囲にする", + "true-fantom/network@_(1) text": "(1) テキスト", + "true-fantom/network@_(3) status ok?": "(3) ステータスOK", + "true-fantom/network@_(4) status": "(4) ステータス", + "true-fantom/network@_(5 1) status text and text": "(5 1) ステータステキストとテキスト", + "true-fantom/network@_(5) status text": "(5) ステータステキスト", + "true-fantom/network@_(6 4) type and status": "(6 4) タイプとステータス", + "true-fantom/network@_(6) type": "(6) タイプ", + "true-fantom/network@_(7) redirected?": "(7) リダイレクトされた", + "true-fantom/network@_(9) body used?": "(9) bodyが使われた", "true-fantom/network@_Network": "ネットワーク", "true-fantom/network@_apple": "りんご", "true-fantom/network@_browser": "ブラウザ", + "true-fantom/network@_connected to internet?": "インターネットに接続されている", + "true-fantom/network@_current url": "現在のURL", "true-fantom/network@_default": "黙認", + "true-fantom/network@_downlink max speed in mb/s": "最大ダウンリンク速度をMB/sで表す", + "true-fantom/network@_downlink speed in mb/s": "ダウンリンク速度をMB/sで表す", + "true-fantom/network@_network generation": "ネットワークの世代", + "true-fantom/network@_network type": "ネットワークタイプ", + "true-fantom/network@_open [USER_URL] in new tab": "[USER_URL]を新しいタブで開く", + "true-fantom/network@_open [USER_URL] in new window with width: [WIDTH] height: [HEIGHT] left: [LEFT] top: [TOP]": "[USER_URL]を新しいウィンドウで幅[WIDTH]高さ[HEIGHT]左[LEFT]トップ[TOP]で開く", + "true-fantom/network@_redirect this tab to [USER_URL]": "このタブを[USER_URL]にリダイレクトする", + "true-fantom/network@_rtt in ms": "ラウンドトリップタイムをミリ秒で表す", + "true-fantom/regexp@_keys": "キー", + "true-fantom/regexp@_values": "値", "utilities@_Utilities": "ユーティリティ", + "utilities@_[PATH] of [JSON_STRING]": "[JSON_STRING]の[PATH]", + "utilities@_clamp [INPUT] between [MIN] and [MAX]": "[INPUT]を[MIN]から[MAX]までで表す", + "utilities@_content from [URL]": "[URL]からのコンテンツ", + "utilities@_current millisecond": "現在のミリ秒", "utilities@_false": "偽", + "utilities@_if [A] then [B] else [C]": "もし[A]なら[B]でなければ[C]", + "utilities@_is [A] exactly [B]?": "[A]が[B]と同一", + "utilities@_letters [START] to [END] of [STRING]": "[STRING]の[START]から[END]番目", "utilities@_pi": "π", + "utilities@_replace [STRING] using the rule [REGEX] with [NEWSTRING]": "[STRING]の[REGEX]を[NEWSTRING]に置き換える", "utilities@_true": "真", + "veggiecan/LongmanDictionary@_all definitions of [word] in the longman dictionary": "ロングマン英英辞典での[word]のすべての定義", + "veggiecan/LongmanDictionary@_primary definition of [word] in the longman dictionary": "ロングマン英英辞典での[word]の定義", "veggiecan/browserfullscreen@_Browser Fullscreen": "ブラウザフルスクリーン", + "veggiecan/browserfullscreen@_[ACTION] fullscreen": "フルスクリーン[ACTION]", "veggiecan/browserfullscreen@_enter": "Enter", + "veggiecan/browserfullscreen@_entered": "に入った", + "veggiecan/browserfullscreen@_exit": "をやめる", + "veggiecan/browserfullscreen@_exited": "をやめた", + "veggiecan/browserfullscreen@_in browser fullscreen?": "ブラウザがフルスクリーン", + "veggiecan/browserfullscreen@_when browser fullscreen [ENABLED]": "ブラウザがフルスクリーン[ENABLED]とき", "veggiecan/mobilekeyboard@_Mobile Keyboard": "モバイルキーボード", - "vercte/dictionaries@_Dictionaries": "辞書" + "veggiecan/mobilekeyboard@_Now the text is different": "今はテキストが違う", + "veggiecan/mobilekeyboard@_You typed: ": "あなたが入力したもの:", + "veggiecan/mobilekeyboard@_alphabetical": "アルファベットの", + "veggiecan/mobilekeyboard@_alphabetical (allows newlines)": "アルファベット (改行を許可) の", + "veggiecan/mobilekeyboard@_close keyboard": "キーボードを閉じる", + "veggiecan/mobilekeyboard@_cursor position/start of selection": "カーソルの場所/選択されている初めの位置", + "veggiecan/mobilekeyboard@_email adress": "メールアドレスの", + "veggiecan/mobilekeyboard@_end of selection": "選択されている最後の位置", + "veggiecan/mobilekeyboard@_is any text selected?": "何かテキストが選択されている", + "veggiecan/mobilekeyboard@_is keyboard open?": "キーボードが開いている", + "veggiecan/mobilekeyboard@_numerical": "数字の", + "veggiecan/mobilekeyboard@_search": "検索の", + "veggiecan/mobilekeyboard@_select text starting at position in text [START] ending at position [END]": "[START]から[END]の場所のテキストを選択する", + "veggiecan/mobilekeyboard@_set cursor position to [INDEX]": "カーソルの位置を[INDEX]にする", + "veggiecan/mobilekeyboard@_set text box's default value to [VALUE]": "テキストボックスのデフォルト値を[VALUE]にする", + "veggiecan/mobilekeyboard@_set textbox current value to [TEXT]": "現在のテキストボックスの値を[TEXT]にする", + "veggiecan/mobilekeyboard@_show [TYPE] keyboard": "[TYPE]キーボードを表示する", + "veggiecan/mobilekeyboard@_show [TYPE] keyboard and wait": "[TYPE]キーボードを表示して待つ", + "veggiecan/mobilekeyboard@_typed text": "入力されたテキスト", + "veggiecan/mobilekeyboard@_web address": "Webアドレスの", + "vercte/dictionaries@_Dictionaries": "辞書", + "vercte/dictionaries@_list of dictionaries": "辞書のリスト", + "vercte/dictionaries@_parse JSON [OBJ] into dictionary [DICT]": "JSON[OBJ]をディクショナリ[DICT]に解析して", + "vercte/dictionaries@_stringify dictionary [DICT] into JSON": "ディクショナリ[DICT]をJSON文字列に変換して" }, "ja-hira": { "-SIPC-/consoles@_Error": "エラー", @@ -3714,15 +3960,15 @@ "-SIPC-/consoles@_Time": "시간", "-SIPC-/consoles@_Warning": "경고", "-SIPC-/consoles@_clear console": "콘솔 비우기", - "-SIPC-/consoles@_create collapsed log group named [string]": "접힌 기록 그룹 [string] 만들기", - "-SIPC-/consoles@_create log group named [string]": "기록 그룹 [string] 만들기", + "-SIPC-/consoles@_create collapsed log group named [string]": "접힌 기록 그룹 [string] 생성하기", + "-SIPC-/consoles@_create log group named [string]": "기록 그룹 [string] 생성하기", "-SIPC-/consoles@_end log timer named [string] and print time elapsed from start to end": "기록 타이머 [string] 끝내고 출력하기", "-SIPC-/consoles@_exit current log group": "현재 기록 그룹을 벗어나기", - "-SIPC-/consoles@_log [string]": "기록 [string]", - "-SIPC-/consoles@_log debug [string]": "디버그 기록 [string]", - "-SIPC-/consoles@_log error [string]": "오류 기록 [string]", - "-SIPC-/consoles@_log information [string]": "정보 기록 [string]", - "-SIPC-/consoles@_log warning [string]": "경고 기록 [string]", + "-SIPC-/consoles@_log [string]": "기록하기 [string]", + "-SIPC-/consoles@_log debug [string]": "디버그 기록하기 [string]", + "-SIPC-/consoles@_log error [string]": "오류 기록하기 [string]", + "-SIPC-/consoles@_log information [string]": "정보 기록하기 [string]", + "-SIPC-/consoles@_log warning [string]": "경고 기록하기 [string]", "-SIPC-/consoles@_print time of log timer named [string]": "기록 타이머 [string] 출력하기", "-SIPC-/consoles@_start log timer named [string]": "기록 타이머 [string] 시작하기", "-SIPC-/time@_April": "4월", @@ -3776,14 +4022,14 @@ "Alestore/nfcwarp@_NFC not supported": "NFC 지원되지 않음", "Alestore/nfcwarp@_NFC supported?": "NFC를 지원하는가?", "Alestore/nfcwarp@_NFCWarp": "NFC워프", - "Alestore/nfcwarp@_Only works in Chrome on Android": "안드로이드의 Chrome 브라우저만 지원함", + "Alestore/nfcwarp@_Only works in Chrome on Android": "안드로이드의 Chrome 브라우저만 지원합니다", "Alestore/nfcwarp@_read NFC tag": "NFC 태그 읽기", "CST1229/images@_Images": "이미지", "CST1229/images@_[QUERY] of image [IMG]": "이미지 [IMG]의 [QUERY]", "CST1229/images@_bottom": "아래", "CST1229/images@_current image ID": "현재 이미지 ID", "CST1229/images@_delete all images": "모든 이미지 삭제하기", - "CST1229/images@_delete image [IMG]": "이미지 [IMG] 삭제하기", + "CST1229/images@_delete image [IMG]": "이미지 [IMG]을(를) 삭제하기", "CST1229/images@_height": "높이", "CST1229/images@_left": "왼쪽", "CST1229/images@_new image from URL [IMAGEURL]": "URL에서의 새 이미지 [IMAGEURL]", @@ -3808,15 +4054,15 @@ "CST1229/zip@_binary": "바이너리", "CST1229/zip@_comment": "주석", "CST1229/zip@_contents of directory [DIR]": "디렉토리 [DIR]의 내용", - "CST1229/zip@_create directory [DIR]": "디렉토리 [DIR] 만들기 ", - "CST1229/zip@_create empty archive named \"archive\"": "빈 아카이브 \"archive\" 만들기", - "CST1229/zip@_create empty archive named [NAME]": "빈 아카이브 [NAME] 만들기", - "CST1229/zip@_current archive name": "현재 아카이브 이름", + "CST1229/zip@_create directory [DIR]": "디렉토리 [DIR] 생성하기", + "CST1229/zip@_create empty archive named \"archive\"": "빈 아카이브 \"archive\" 생성하기", + "CST1229/zip@_create empty archive named [NAME]": "빈 아카이브 [NAME] 생성하기", + "CST1229/zip@_current archive name": "현재 아카이브의 이름", "CST1229/zip@_current directory path": "현재 디렉토리 경로", "CST1229/zip@_currently open archives": "열린 아카이브 목록", - "CST1229/zip@_delete [FILE]": "[FILE] 삭제하기", + "CST1229/zip@_delete [FILE]": "[FILE]을(를) 삭제하기", "CST1229/zip@_error opening archive?": "아카이브를 여는 중 오류가 발생했는가?", - "CST1229/zip@_file [FILE] as [TYPE]": "파일 [FILE]을(를) [TYPE](으)로 읽기", + "CST1229/zip@_file [FILE] as [TYPE]": "파일 [FILE]을(를) [TYPE] 형식으로", "CST1229/zip@_folder": "폴더", "CST1229/zip@_go to directory [DIR]": "디렉토리 [DIR](으)로 이동하기", "CST1229/zip@_hex": "Hex", @@ -3826,17 +4072,17 @@ "CST1229/zip@_name": "이름", "CST1229/zip@_no compression (fastest)": "압축 없음 (가장 빠름)", "CST1229/zip@_open archive from zip [TYPE] [DATA] named [NAME]": "아카이브 [NAME]을(를) [TYPE][DATA](으)로 열기 ", - "CST1229/zip@_open zip from [TYPE] [DATA] named \"archive\"": "아카이브 \"아카이브\"를 [TYPE][DATA](으)로 열기", + "CST1229/zip@_open zip from [TYPE] [DATA] named \"archive\"": "아카이브 \"archive\"를 [TYPE][DATA](으)로 열기", "CST1229/zip@_other archive": "다른 아카이브", "CST1229/zip@_output zip type [TYPE] compression level [COMPRESSION]": "zip 타입[TYPE] 압축 레벨[COMPRESSION] (으)로 출력", "CST1229/zip@_path": "경로", "CST1229/zip@_path [PATH] from [ORIGIN]": "[ORIGIN]에서 [PATH] 경로로", "CST1229/zip@_remove all archives": "모든 아카이브 삭제하기", - "CST1229/zip@_remove current archive": "현재 아카이브 삭제하기", + "CST1229/zip@_remove current archive": "현재 아카이브를 삭제하기", "CST1229/zip@_set [META] of [FILE] to [VALUE]": "[FILE]의 [META]을(를) [VALUE](으)로 정하기", "CST1229/zip@_set archive comment to [COMMENT]": "아카이브 주석을 [COMMENT](으)로 정하기", "CST1229/zip@_string": "문자열", - "CST1229/zip@_switch to archive named [NAME]": "아카이브 [NAME](으)로 바꾸기 ", + "CST1229/zip@_switch to archive named [NAME]": "아카이브 [NAME](으)로 전환하기", "CST1229/zip@_text": "텍스트", "CST1229/zip@_unix modified timestamp": "unix 수정 타임스탬프", "CST1229/zip@_write file [FILE] content [CONTENT] type [TYPE]": "새 파일 [FILE]을(를) [TYPE] 타입의 [CONTENT](으)로 쓰기", @@ -3867,7 +4113,7 @@ "CubesterYT/TurboHook@_icon": "아이콘", "CubesterYT/TurboHook@_name": "이름", "CubesterYT/WindowControls@_Hello World!": "헬로 월드!", - "CubesterYT/WindowControls@_May not work in normal browser tabs": "일반 브라우저 탭에서 작동하지 않음", + "CubesterYT/WindowControls@_May not work in normal browser tabs": "일반 브라우저 탭에서 작동하지 않습니다", "CubesterYT/WindowControls@_Refer to Documentation for details": "문서에서 더 알아보기", "CubesterYT/WindowControls@_Window Controls": "창 제어", "CubesterYT/WindowControls@_bottom": "아래", @@ -3885,6 +4131,7 @@ "CubesterYT/WindowControls@_is window fullscreen?": "전체 화면인가?", "CubesterYT/WindowControls@_is window touching screen edge?": "창이 화면의 끝에 닿았는가?", "CubesterYT/WindowControls@_left": "왼쪽", + "CubesterYT/WindowControls@_match stage size": "무대 크기 맞추기", "CubesterYT/WindowControls@_move window to the [PRESETS]": "창을 [PRESETS](으)로 이동하기", "CubesterYT/WindowControls@_move window to x: [X] y: [Y]": "창을 x:[X] y:[Y] (으)로 이동하기", "CubesterYT/WindowControls@_random position": "무작위 위치", @@ -3924,7 +4171,7 @@ "DT/cameracontrols@_move camera [val] steps": "카메라를 [val] 만큼 움직이기", "DT/cameracontrols@_move camera to [sprite]": "카메라를 [sprite](으)로 이동하기", "DT/cameracontrols@_no sprites exist": "(스프라이트 없음)", - "DT/cameracontrols@_point camera towards [sprite]": "카메라를 [sprite]쪽으로 향하기", + "DT/cameracontrols@_point camera towards [sprite]": "카메라를 [sprite]쪽으로 바라보기", "DT/cameracontrols@_set background color to [val]": "배경색을 [val](으)로 정하기", "DT/cameracontrols@_set camera direction to [val]": "카메라의 방향을 [val](으)로 정하기", "DT/cameracontrols@_set camera to x: [x] y: [y]": "카메라를 x:[x] y:[y] (으)로 이동하기", @@ -4019,9 +4266,11 @@ "Lily/ListTools@_delete all [ITEM] in [LIST]": "[LIST]의 모든 [ITEM] 삭제하기", "Lily/ListTools@_delete items [NUM1] to [NUM2] of [LIST]": "[LIST]의 [NUM1]번째부터 [NUM2]번째까지 삭제하기", "Lily/ListTools@_descending": "내림차순", + "Lily/ListTools@_first": "첫째", "Lily/ListTools@_for each item # [VAR] in [LIST]": "[LIST]의 각 항목의 번째를 [VAR](으)로 순회하기", "Lily/ListTools@_for each item value [VAR] in [LIST]": "[LIST]의 각 항목의 값을 [VAR](으)로 순회하기", "Lily/ListTools@_item [NUM] exists in [LIST]?": "[LIST]에서 [NUM]이(가) 존재하는가?", + "Lily/ListTools@_last": "마지막째", "Lily/ListTools@_list [LIST] joined by [STRING]": "[LIST]을(를) [STRING](으)로 이어붙임", "Lily/ListTools@_order of [LIST] is [ORDER]?": "[LIST]이(가) [ORDER]인가?", "Lily/ListTools@_random": "무작위", @@ -4035,7 +4284,7 @@ "Lily/LooksPlus@_Looks+": "형태 플러스", "Lily/LooksPlus@_[ATTRIBUTE] of [COSTUME]": "[COSTUME]의 [ATTRIBUTE]", "Lily/LooksPlus@_[CONTENT] of costume # [COSTUME] of [TARGET]": "[TARGET]의 모양 [COSTUME]번째의 [CONTENT]", - "Lily/LooksPlus@_[EFFECT] effect of [TARGET]": "[TARGET]의 [EFFECT] 효과값", + "Lily/LooksPlus@_[EFFECT] effect of [TARGET]": "[TARGET]의 [EFFECT] 효과 값", "Lily/LooksPlus@_[TARGET] visible?": "[TARGET]이(가) 보이는가?", "Lily/LooksPlus@_brightness": "밝기", "Lily/LooksPlus@_color": "색깔", @@ -4090,46 +4339,48 @@ "Lily/MoreTimers@_More Timers": "추가 타이머", "Lily/MoreTimers@_change timer [TIMER] by [NUM]": "타이머 [TIMER]을(를) [NUM]만큼 바꾸기", "Lily/MoreTimers@_list existing timers": "존재하는 타이머 목록", - "Lily/MoreTimers@_pause timer [TIMER]": "타이머 [TIMER] 일시정지 하기", + "Lily/MoreTimers@_pause timer [TIMER]": "타이머 [TIMER] 일시정지하기", "Lily/MoreTimers@_remove all timers": "모든 타이머 제거하기", - "Lily/MoreTimers@_remove timer [TIMER]": "타이머 [TIMER] 제거하기", - "Lily/MoreTimers@_resume timer [TIMER]": "타이머 [TIMER] 재시작 하기", + "Lily/MoreTimers@_remove timer [TIMER]": "타이머 [TIMER]을(를) 제거하기", + "Lily/MoreTimers@_resume timer [TIMER]": "타이머 [TIMER] 재시작하기", "Lily/MoreTimers@_set timer [TIMER] to [NUM]": "타이머 [TIMER]을(를) [NUM](으)로 정하기", - "Lily/MoreTimers@_start/reset timer [TIMER]": "타이머 [TIMER] 시작/초기화 하기", - "Lily/MoreTimers@_timer [TIMER]": "타이머 [TIMER]", + "Lily/MoreTimers@_start/reset timer [TIMER]": "타이머 [TIMER] 시작·초기화하기", + "Lily/MoreTimers@_timer [TIMER]": "타이머 [TIMER] 값", "Lily/MoreTimers@_timer [TIMER] exists?": "타이머 [TIMER]이(가) 존재하는가?", - "Lily/MoreTimers@_when timer [TIMER] [OP] [NUM]": "타이머가 [TIMER][OP][NUM] 일 때", + "Lily/MoreTimers@_when timer [TIMER] [OP] [NUM]": "타이머 [TIMER]이(가) [OP][NUM] 일 때", "Lily/Skins@_Skins": "스킨", "Lily/Skins@_[ATTRIBUTE] of skin [NAME]": "스킨 [NAME]의 [ATTRIBUTE]", - "Lily/Skins@_create SVG skin [SVG] as [NAME]": "SVG 스킨 [NAME] 만들기 [SVG] ", + "Lily/Skins@_create SVG skin [SVG] as [NAME]": "SVG 스킨 [NAME] 생성하기 [SVG] ", "Lily/Skins@_current skin of [TARGET]": "현재 스킨의 [TARGET]", "Lily/Skins@_delete all skins": "모든 스킨 삭제하기", - "Lily/Skins@_delete skin [NAME]": "스킨 [NAME] 삭제하기", + "Lily/Skins@_delete skin [NAME]": "스킨 [NAME]을(를) 삭제하기", "Lily/Skins@_height": "높이", "Lily/Skins@_load skin from URL [URL] as [NAME]": "스킨 [NAME]을(를) URL에서 불러오기 [URL]", - "Lily/Skins@_load skin from [COSTUME] as [NAME]": "스킨 [NAME]을(를) [COSTUME](으)로 불러오기", + "Lily/Skins@_load skin from [COSTUME] as [NAME]": "스킨 [COSTUME]을(를) [NAME](으)로 불러오기", "Lily/Skins@_restore skin of [TARGET]": "[TARGET]의 스킨 복구하기", "Lily/Skins@_set skin of [TARGET] to [NAME]": "[TARGET]의 스킨을 [NAME](으)로 정하기", + "Lily/Skins@_skin [NAME] is loaded?": "스킨 [NAME]이(가) 불러왔는가?", "Lily/Skins@_width": "넓이", "Lily/SoundExpanded@_Sound Expanded": "소리 확장", "Lily/SoundExpanded@_[ATTRIBUTE] of [SOUND]": "[SOUND]의 [ATTRIBUTE]", "Lily/SoundExpanded@_[SOUND] is looping?": "[SOUND]이(가) 반복 재생인가?", - "Lily/SoundExpanded@_change project volume by [VALUE]": "프로젝트 볼륨을 [VALUE]만큼 바꾸기 ", + "Lily/SoundExpanded@_change project volume by [VALUE]": "프로젝트의 음량을 [VALUE]만큼 바꾸기 ", "Lily/SoundExpanded@_channels": "채널 수", "Lily/SoundExpanded@_effect [EFFECT] of [TARGET]": "[TARGET]의 [EFFECT]효과", "Lily/SoundExpanded@_end looping [SOUND]": "[SOUND] 반복 멈추기", "Lily/SoundExpanded@_length": "재생 길이", "Lily/SoundExpanded@_pan": "소리 좌우 위치", - "Lily/SoundExpanded@_pause all sounds": "모든 소리 일시정지 하기", + "Lily/SoundExpanded@_pause all sounds": "모든 소리 일시정지하기", "Lily/SoundExpanded@_pitch": "음 높이", "Lily/SoundExpanded@_play sound [SOUND] from [START] seconds until done": "소리 [SOUND]을(를) [START]초부터 끝까지 재생하기", "Lily/SoundExpanded@_play sound [SOUND] from [START] to [END] seconds until done": "소리 [SOUND]을(를) [START]초부터 끝나기 [END]초 전까지 재생하기", - "Lily/SoundExpanded@_project volume": "프로젝트 볼륨", - "Lily/SoundExpanded@_resume all sounds": "모든 소리 재개하기", + "Lily/SoundExpanded@_project volume": "프로젝트의 음량", + "Lily/SoundExpanded@_resume all sounds": "모든 소리 재시작하기", "Lily/SoundExpanded@_sample rate": "샘플레이트", - "Lily/SoundExpanded@_set project volume to [VALUE]%": "프로젝트 음량을 [VALUE]% 로 정하기", + "Lily/SoundExpanded@_set project volume to [VALUE]%": "프로젝트의 음량을 [VALUE]% 로 정하기", "Lily/SoundExpanded@_sound [SOUND] is playing?": "소리 [SOUND]이(가) 재생 중인가?", "Lily/SoundExpanded@_start looping [SOUND]": "[SOUND] 반복 재생하기", + "Lily/SoundExpanded@_start looping [SOUND] loop region [START] to [END] seconds": "소리 [SOUND]을(를) [START]초부터 [END]초까지의 구간에서 반복 재생하기 ", "Lily/SoundExpanded@_start sound [SOUND] from [START] to [END] seconds": "소리 [SOUND]을(를) [START]초부터 [END]초까지 재생하기", "Lily/SoundExpanded@_stop sound [SOUND]": "소리 [SOUND] 끄기", "Lily/TempVariables2@_Runtime Variables": "런타임 변수", @@ -4152,15 +4403,18 @@ "Lily/Video@_[ATTRIBUTE] of video [NAME]": "비디오 [NAME]의 [ATTRIBUTE]", "Lily/Video@_current time": "현재 재생 시간", "Lily/Video@_current video on [TARGET]": "[TARGET]의 현재 비디오", - "Lily/Video@_delete video [NAME]": "비디오 [NAME] 삭제하기", + "Lily/Video@_delete video [NAME]": "비디오 [NAME]을(를) 삭제하기", "Lily/Video@_duration": "재생 길이", "Lily/Video@_height": "높이", "Lily/Video@_load video from URL [URL] as [NAME]": "비디오 [NAME]을(를) URL에서 불러오기 [URL]", "Lily/Video@_loaded videos": "불러온 비디오 목록", - "Lily/Video@_pause video [NAME]": "비디오 [NAME] 일시정지 하기", + "Lily/Video@_pause video [NAME]": "비디오 [NAME] 일시정지하기", "Lily/Video@_paused": "일시정지 중", + "Lily/Video@_playback rate": "재생 속도", "Lily/Video@_playing": "재생 중", "Lily/Video@_resume video [NAME]": "비디오 [NAME] 재시작하기", + "Lily/Video@_screenshot of video [NAME] at current time": "비디오 [NAME]의 현재 화면 캡쳐하기", + "Lily/Video@_set playback rate of video [NAME] to [RATE]": "비디오 [NAME]의 재생 속도를 [RATE](으)로 정하기", "Lily/Video@_set volume of video [NAME] to [VALUE]": "비디오 [NAME]의 음량을 [VALUE](으)로 정하기", "Lily/Video@_show video [NAME] on [TARGET]": "비디오 [NAME]을(를) [TARGET]에 보이기", "Lily/Video@_start video [NAME] at [DURATION] seconds": "비디오 [NAME]을(를) [DURATION]초 부터 시작하기", @@ -4172,25 +4426,27 @@ "Lily/lmsutils@_Hide Legacy Blocks": "레거시 블록 숨기기", "Lily/lmsutils@_Lily's Toolbox": "Lily의 도구상자", "Lily/lmsutils@_Show Legacy Blocks": "레거시 블록 보이기", - "Lily/lmsutils@_[DROPDOWN] of user": "사용자 [DROPDOWN]", + "Lily/lmsutils@_[DROPDOWN] of user": "사용자의 [DROPDOWN]", "Lily/lmsutils@_[STRING] to lowercase": "[STRING] 소문자로", "Lily/lmsutils@_[STRING] to uppercase": "[STRING] 대문자로", - "Lily/lmsutils@_alert [STRING]": "알림 상자 [STRING]", + "Lily/lmsutils@_alert [STRING]": "알림 보이기 [STRING]", "Lily/lmsutils@_angle [ANGLE]": "방향 [ANGLE]", "Lily/lmsutils@_binary": "바이너리", "Lily/lmsutils@_brightness": "밝기", "Lily/lmsutils@_browser": "브라우저", "Lily/lmsutils@_change variable [INPUTA] by [INPUTB]": "변수 [INPUTA]을(를) [INPUTB]만큼 바꾸기", + "Lily/lmsutils@_clamp [INPUTA] between [INPUTB] and [INPUTC]": "[INPUTA] 값을 [INPUTB]부터 [INPUTC]까지 범위로 제한", "Lily/lmsutils@_clear console": "콘솔 비우기", "Lily/lmsutils@_clipboard": "클립보드", "Lily/lmsutils@_clone count": "복제본 개수", "Lily/lmsutils@_color": "색깔", "Lily/lmsutils@_color [COLOUR]": "색 [COLOUR]", "Lily/lmsutils@_confirm [STRING]": "확인 상자 [STRING]", - "Lily/lmsutils@_decode [STRING] from [DROPDOWN]": "[STRING]을(를) [DROPDOWN]에서 디코딩하기", + "Lily/lmsutils@_decode [STRING] from [DROPDOWN]": "[STRING]을(를) [DROPDOWN]에서 디코딩", "Lily/lmsutils@_delete all variables": "모든 변수 삭제하기", "Lily/lmsutils@_delete variable [INPUT]": "변수 [INPUT] 삭제하기", - "Lily/lmsutils@_encode [STRING] to [DROPDOWN]": "[STRING]을(를) [DROPDOWN](으)로 인코딩하기", + "Lily/lmsutils@_effect [INPUT]": "[INPUT]효과 값 ", + "Lily/lmsutils@_encode [STRING] to [DROPDOWN]": "[STRING]을(를) [DROPDOWN](으)로 인코딩", "Lily/lmsutils@_false": "거짓", "Lily/lmsutils@_fisheye": "어안 렌즈", "Lily/lmsutils@_ghost": "투명도", @@ -4206,7 +4462,7 @@ "Lily/lmsutils@_lowercase": "소문자", "Lily/lmsutils@_matrix [MATRIX]": "격자 [MATRIX]", "Lily/lmsutils@_mosaic": "모자이크", - "Lily/lmsutils@_newline character": "개행 문자", + "Lily/lmsutils@_newline character": "줄바꿈 문자", "Lily/lmsutils@_note [NOTE]": "음표 [NOTE]", "Lily/lmsutils@_number": "숫자", "Lily/lmsutils@_open link [INPUT] in new tab": "새 탭에서 링크 [INPUT] 열기", @@ -4218,7 +4474,7 @@ "Lily/lmsutils@_redirect to link [INPUT]": "링크 [INPUT](으)로 리다이렉트하기", "Lily/lmsutils@_replace first [INPUTA] with [INPUTB] in [STRING]": "[INPUTB]의 첫번째 [INPUTA]을(를) [STRING](으)로 바꾸기", "Lily/lmsutils@_reverse [STRING]": "[STRING] 거꾸로", - "Lily/lmsutils@_screen [DROPDOWN]": "화면 [DROPDOWN]", + "Lily/lmsutils@_screen [DROPDOWN]": "화면의 [DROPDOWN]", "Lily/lmsutils@_set [STRING] to clipboard": "클립보드에 [STRING] 복사하기", "Lily/lmsutils@_set username to [INPUT]": "사용자 이름을 [INPUT](으)로 정하기", "Lily/lmsutils@_set variable [INPUTA] to [INPUTB]": "변수 [INPUTA]을(를) [INPUTB](으)로 정하기", @@ -4231,7 +4487,7 @@ "Lily/lmsutils@_when key [KEY_OPTION] pressed": "[KEY_OPTION]키를 눌렀을 때", "Lily/lmsutils@_whirl": "소용돌이", "Lily/lmsutils@_width": "넓이", - "Lily/lmsutils@_window [DROPDOWN]": "창 [DROPDOWN]", + "Lily/lmsutils@_window [DROPDOWN]": "창의 [DROPDOWN]", "Longboost/color_channels@_RGB Channels": "RGB 채널", "Longboost/color_channels@_[COLOR] channel enabled?": "[COLOR] 채널이 활성화 되었는가?", "Longboost/color_channels@_blue": "Blue", @@ -4275,7 +4531,7 @@ "NexusKitten/moremotion@_height": "높이", "NexusKitten/moremotion@_move [PERCENT]% of the way to x: [X] y: [Y]": "x:[X] y:[Y] (으)로 [PERCENT]% 만큼 이동하기", "NexusKitten/moremotion@_move [STEPS] steps towards x: [X] y: [Y]": "x:[X] y:[Y] (으)로 [STEPS]번 나눠 이동하기 ", - "NexusKitten/moremotion@_point towards x: [X] y: [Y]": "x:[X] y:[Y] 쪽을 향하기", + "NexusKitten/moremotion@_point towards x: [X] y: [Y]": "x:[X] y:[Y] 쪽 바라보기", "NexusKitten/moremotion@_rotation style": "회전 방식", "NexusKitten/moremotion@_sprite [WHAT]": "스프라이트 [WHAT]", "NexusKitten/moremotion@_touching rectangle x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?": "사각형 x1:[X1] y1:[Y1] x2:[X2] y2:[Y2] 에 닿았는가?", @@ -4288,10 +4544,10 @@ "NexusKitten/sgrab@_favorite": "마음에 들어요", "NexusKitten/sgrab@_follower": "팔로워", "NexusKitten/sgrab@_following": "팔로잉", - "NexusKitten/sgrab@_global [WHAT] ranking for [WHO]": "[WHO]의 [WHAT] 세계 순위 얻기", - "NexusKitten/sgrab@_global [WHAT] ranking for project id [WHO]": "프로젝트 ID [WHO]의 [WHAT]의 세계 순위 얻기", - "NexusKitten/sgrab@_grab [WHAT] count of project id [WHO]": "프로젝트 ID [WHO]의 [WHAT] 얻기", - "NexusKitten/sgrab@_grab [WHAT] count of user [WHO]": "사용자 [WHO]의 [WHAT] 얻기", + "NexusKitten/sgrab@_global [WHAT] ranking for [WHO]": "[WHO]의 [WHAT] 세계 순위 값", + "NexusKitten/sgrab@_global [WHAT] ranking for project id [WHO]": "프로젝트 ID [WHO]의 [WHAT]의 세계 순위 값", + "NexusKitten/sgrab@_grab [WHAT] count of project id [WHO]": "프로젝트 ID [WHO]의 [WHAT] 값", + "NexusKitten/sgrab@_grab [WHAT] count of user [WHO]": "사용자 [WHO]의 [WHAT] 값", "NexusKitten/sgrab@_location": "국가", "NexusKitten/sgrab@_love": "좋아요", "NexusKitten/sgrab@_name of project id [WHO]": "프로젝트 ID [WHO]의 이름", @@ -4306,9 +4562,9 @@ "SharkPool/Font-Manager@_Font Manager": "글꼴 매니저", "SharkPool/Font-Manager@_[ADDED] fonts": "[ADDED]된 글꼴", "SharkPool/Font-Manager@_[DATA] of font [NAME]": "글꼴 [NAME]의 [DATA]", - "SharkPool/Font-Manager@_add font named [NAME] with fallback [BACKUP] from URL [URL]": "시스템 폰트 [NAME] 추가하거나 URL에서 [BACKUP](으)로 대체하기 [URL]", - "SharkPool/Font-Manager@_add system font named [NAME] with fallback [BACKUP]": "시스템 폰트 [NAME] 추가하거나 [BACKUP](으)로 대체하기", - "SharkPool/Font-Manager@_all custom fonts": "사용자 지정 폰트 목록", + "SharkPool/Font-Manager@_add font named [NAME] with fallback [BACKUP] from URL [URL]": "시스템 글꼴 [NAME] 추가하거나 URL의 [BACKUP](으)로 대체하기 [URL]", + "SharkPool/Font-Manager@_add system font named [NAME] with fallback [BACKUP]": "시스템 글꼴 [NAME] 추가하거나 [BACKUP](으)로 대체하기", + "SharkPool/Font-Manager@_all custom fonts": "사용자 지정 글꼴 목록", "SharkPool/Font-Manager@_data: uri": "data: URI", "SharkPool/Font-Manager@_fallback": "대체 글꼴", "SharkPool/Font-Manager@_font [NAME] added?": "글꼴 [NAME]이(가) 추가되었는가?", @@ -4324,7 +4580,6 @@ "Skyhigh173/bigint@_Arithmetic": "산술 연산", "Skyhigh173/bigint@_Bitwise": "비트 연산", "Skyhigh173/bigint@_Logic": "논리 연산", - "Skyhigh173/bigint@_[a] mod [b]": "[a] 나누기 [b]의 나머지", "Skyhigh173/bigint@_convert BigInt [text] to number": "BigInt [text]을(를) 숫자로", "Skyhigh173/bigint@_convert number [text] to BigInt": "숫자 [text]을(를) BigInt로", "Skyhigh173/json@_Advanced": "고급 설정", @@ -4340,7 +4595,7 @@ "Skyhigh173/json@_array from text [json]": "텍스트를 배열화 [json]", "Skyhigh173/json@_ascending": "오름차순", "Skyhigh173/json@_datas": "데이터", - "Skyhigh173/json@_delete [item] in [json]": "[json]의 [item] 삭제하기", + "Skyhigh173/json@_delete [item] in [json]": "[json]의 [item]을(를) 삭제하기", "Skyhigh173/json@_delete all [item] in array [json]": "배열 [json]의 [item]을(를) 모두 삭제하기", "Skyhigh173/json@_delete item [item] of array [json]": "배열 [json]의 [item]번째를 삭제하기", "Skyhigh173/json@_descending": "내림차순", @@ -4355,11 +4610,11 @@ "Skyhigh173/json@_keys": "키", "Skyhigh173/json@_length of array [json]": "배열 [json]의 길이", "Skyhigh173/json@_length of json [json]": "JSON [json]의 길이", - "Skyhigh173/json@_new [json]": "새 [json]", + "Skyhigh173/json@_new [json]": "새로운 [json]", "Skyhigh173/json@_replace item [pos] of [json] with [item]": "배열 [json]의 [pos]번째 값을 [item](으)로 정하기", "Skyhigh173/json@_reverse array [json]": "역방향 배열 [json]", "Skyhigh173/json@_select a list": "(리스트 선택하기)", - "Skyhigh173/json@_set [item] in [json] to [value]": "[json]의 [item]의 값을 [value](으)로 정하기", + "Skyhigh173/json@_set [item] in [json] to [value]": "[json]에서 [item]의 값을 [value](으)로 정하기", "Skyhigh173/json@_set length of array [json] to [len]": "배열 [json]의 길이를 [len](으)로 정하기", "Skyhigh173/json@_set list [list] to [json]": "리스트 [list]을(를) [json](으)로 정하기", "Skyhigh173/json@_sort array [list] in [order] order": "배열 [list]을(를) [order]으로 정렬하기", @@ -4368,19 +4623,28 @@ "TheShovel/CanvasEffects@_Canvas Effects": "Canvas 효과", "TheShovel/CanvasEffects@_blur": "흐림", "TheShovel/CanvasEffects@_border color": "외곽선 색상", - "TheShovel/CanvasEffects@_border radius": "외곽선 둥글기", + "TheShovel/CanvasEffects@_border radius": "모서리 둥글기", "TheShovel/CanvasEffects@_border style": "외곽선 스타일", "TheShovel/CanvasEffects@_border width": "외곽선 두께", "TheShovel/CanvasEffects@_brightness": "밝기", "TheShovel/CanvasEffects@_change canvas [EFFECT] by [NUMBER]": "캔버스 [EFFECT]효과를 [NUMBER]만큼 바꾸기", "TheShovel/CanvasEffects@_color shift": "색깔", "TheShovel/CanvasEffects@_contrast": "대비", + "TheShovel/CanvasEffects@_dashed": "파선", "TheShovel/CanvasEffects@_default": "기본", + "TheShovel/CanvasEffects@_dotted": "점선", + "TheShovel/CanvasEffects@_double": "복선", "TheShovel/CanvasEffects@_get canvas [EFFECT]": "캔버스 [EFFECT]효과 값 ", + "TheShovel/CanvasEffects@_groove": "오목 틀", + "TheShovel/CanvasEffects@_inset": "오목 요소", "TheShovel/CanvasEffects@_invert": "반전", + "TheShovel/CanvasEffects@_none": "없음", "TheShovel/CanvasEffects@_offset X": "위치 x", "TheShovel/CanvasEffects@_offset Y": "위치 y", + "TheShovel/CanvasEffects@_outset": "볼록 요소", "TheShovel/CanvasEffects@_pixelated": "픽셀화", + "TheShovel/CanvasEffects@_resize rendering mode": "렌더링 모드", + "TheShovel/CanvasEffects@_ridge": "볼록 틀", "TheShovel/CanvasEffects@_rotation": "회전", "TheShovel/CanvasEffects@_saturation": "채도", "TheShovel/CanvasEffects@_scale": "크기", @@ -4393,18 +4657,19 @@ "TheShovel/CanvasEffects@_set canvas resize rendering mode [EFFECT]": "캔버스 렌더링 모드를 [EFFECT](으)로 정하기", "TheShovel/CanvasEffects@_skew X": "기울기 x", "TheShovel/CanvasEffects@_skew Y": "기울기 y", + "TheShovel/CanvasEffects@_solid": "실선", "TheShovel/CanvasEffects@_transparency": "투명", "TheShovel/ColorPicker@_Color Picker": "색상 선택기", "TheShovel/ColorPicker@_blue": "Blue", "TheShovel/ColorPicker@_color [TYPE] value": "선택기 색상의 [TYPE]값", "TheShovel/ColorPicker@_green": "Green", "TheShovel/ColorPicker@_hex": "Hex코드", - "TheShovel/ColorPicker@_picker [COORD] position": "선택기 [COORD]좌표 값", + "TheShovel/ColorPicker@_picker [COORD] position": "선택기의 [COORD]좌표 값", "TheShovel/ColorPicker@_red": "Red", "TheShovel/ColorPicker@_set picker color to [COLOR]": "선택기 색상을 [COLOR](으)로 정하기", "TheShovel/ColorPicker@_set picker position to x: [X] y: [Y]": "선택기를 x:[X] y:[Y] (으)로 이동하기", "TheShovel/ColorPicker@_show color picker": "색상 선택기 보이기", - "TheShovel/ColorPicker@_when color changed": "색상이 변경되었을 때", + "TheShovel/ColorPicker@_when color changed": "색상이 바뀌었을 때", "TheShovel/CustomStyles@_Custom Styles": "사용자 지정 스타일", "TheShovel/CustomStyles@_ask prompt background": "묻기 입력란 배경", "TheShovel/CustomStyles@_ask prompt background border width": "묻기 입력란 배경 외곽선 두께", @@ -4454,15 +4719,18 @@ "TheShovel/LZ-String@_compress [TEXT] to [TYPE]": "[TEXT]을(를) [TYPE](으)로 압축", "TheShovel/LZ-String@_decompress [TEXT] from [TYPE]": "[TEXT]을(를) [TYPE]에서 압축 해제", "TheShovel/ShovelUtils@_Link or data URI here": "링크 또는 dataURI 입력", - "TheShovel/ShovelUtils@_all sprites": "모든 스프라이트", + "TheShovel/ShovelUtils@_all sprites": "모든 스프라이트 목록", "TheShovel/ShovelUtils@_brightness of [color]": "[color]의 밝기", - "TheShovel/ShovelUtils@_delete costume [COSNAME] in [SPRITE]": "[SPRITE]의 모양 [COSNAME] 삭제하기", - "TheShovel/ShovelUtils@_delete sprite [SPRITE]": "스프라이트 [SPRITE] 삭제하기", + "TheShovel/ShovelUtils@_delete costume [COSNAME] in [SPRITE]": "[SPRITE]의 모양 [COSNAME]을(를) 삭제하기", + "TheShovel/ShovelUtils@_delete sprite [SPRITE]": "스프라이트 [SPRITE]을(를) 삭제하기", + "TheShovel/ShovelUtils@_import image from [TEXT] name [NAME]": "모양 [NAME]을(를) [TEXT]에서 불러오기", "TheShovel/ShovelUtils@_import project from [TEXT]": "프로젝트를 [TEXT]에서 불러오기", "TheShovel/ShovelUtils@_import sound from [TEXT] name [NAME]": "소리 [NAME]을(를) [TEXT]에서 불러오기 ", "TheShovel/ShovelUtils@_import sprite from [TEXT]": "스프라이트를 [TEXT]에서 불러오기", "TheShovel/ShovelUtils@_list [TEXT] as array": "리스트 [TEXT](을)를 배열로", "TheShovel/ShovelUtils@_load extension from [TEXT]": "확장 기능을 [TEXT]에서 불러오기", + "TheShovel/ShovelUtils@_restart project": "프로젝트 재시작하기", + "TheShovel/ShovelUtils@_set editing target to [NAME]": "편집기 대상을 [NAME](으)로 정하기", "TheShovel/ShovelUtils@_set list [NAME] to [TEXT]": "리스트 [NAME]을(를) [TEXT](으)로 정하기", "Xeltalliv/clippingblending@_Clipping & Blending": "클리핑 및 블렌딩", "Xeltalliv/clippingblending@_additive": "더하기", @@ -4487,13 +4755,13 @@ "Xeltalliv/clippingblending@_width": "넓이", "XeroName/Deltatime@_Delta Time": "델타 타임", "XmerOriginals/closecontrol@_Ask Before Closing Tab": "탭 닫기 전에 묻기", - "XmerOriginals/closecontrol@_ask before closing tab enabled?": "탭을 닫기 전에 묻는가?", + "XmerOriginals/closecontrol@_ask before closing tab enabled?": "탭 닫기 전에 묻기가 활성화인가?", "XmerOriginals/closecontrol@_disabled": "비활성화", "XmerOriginals/closecontrol@_enabled": "활성화", "XmerOriginals/closecontrol@_set ask before closing tab to [OPTION]": "탭 닫기 전에 묻기 [OPTION] ", "ZXMushroom63/searchApi@_Search Params": "검색 파라미터", "ZXMushroom63/searchApi@_append search parameter [ID] with value [VAL]": "검색 파라미터 [ID](으)로 [VAL] 추가하기", - "ZXMushroom63/searchApi@_delete search parameter [ID]": "검색 파라미터 [ID] 삭제하기", + "ZXMushroom63/searchApi@_delete search parameter [ID]": "검색 파라미터 [ID]을(를) 삭제하기", "ZXMushroom63/searchApi@_has search parameter [ID]?": "검색 파라미터 [ID]이(가) 존재하는가?", "ZXMushroom63/searchApi@_index [I] of search parameters [ID]": "검색 파라미터 [ID]의 [I]번째 값", "ZXMushroom63/searchApi@_length of search parameters": "모든 검색 파라미터 개수", @@ -4523,35 +4791,78 @@ "bitwise@_[CENTRAL] to binary": "[CENTRAL]을(를) 이진수로", "bitwise@_[CENTRAL] to number": "[CENTRAL]을(를) 숫자로", "bitwise@_is [CENTRAL] binary?": "[CENTRAL]이(가) 이진수인가?", + "box2d@griffpatch.applyAngForce": "힘 [force] 만큼 회전하기", + "box2d@griffpatch.applyForce": "힘을 [force] 만큼 [dir] 방향으로 가하기 ", "box2d@griffpatch.categoryName": "물리", + "box2d@griffpatch.changeScroll": "화면 스크롤을 x: [ox] y: [oy] 만큼 바꾸기", "box2d@griffpatch.changeVelocity": "속도를 x:[sx] y:[sy] 만큼 바꾸기", - "box2d@griffpatch.getFriction": "마찰", + "box2d@griffpatch.disablePhysics": "이 스프라이트에 물리 해제하기", + "box2d@griffpatch.doTick": "물리 연산하기", + "box2d@griffpatch.getAngVelocity": "각속도", + "box2d@griffpatch.getDensity": "밀도", + "box2d@griffpatch.getFriction": "마찰력", "box2d@griffpatch.getGravityX": "중력 x", "box2d@griffpatch.getGravityY": "중력 y", - "box2d@griffpatch.getRestitution": "탄성", + "box2d@griffpatch.getRestitution": "탄성력", + "box2d@griffpatch.getScrollX": "화면 스크롤 x", + "box2d@griffpatch.getScrollY": "화면 스크롤 y", + "box2d@griffpatch.getStatic": "고정되었는가?", + "box2d@griffpatch.getTickRate": "물리 연산 속도", + "box2d@griffpatch.getTouching": "[where]에 닿은 스프라이트 목록", "box2d@griffpatch.getVelocityX": "속도 x", "box2d@griffpatch.getVelocityY": "속도 y", + "box2d@griffpatch.setAngVelocity": "각속도를 [force](으)로 정하기", + "box2d@griffpatch.setDensity": "밀도를 [density](으)로 정하기", + "box2d@griffpatch.setDensityValue": "밀도를 [density](으)로 정하기", + "box2d@griffpatch.setFriction": "마찰력을 [friction](으)로 정하기", + "box2d@griffpatch.setFrictionValue": "마찰력을 [friction](으)로 정하기", "box2d@griffpatch.setGravity": "중력을 x:[gx] y:[gy] (으)로 정하기", + "box2d@griffpatch.setPhysics": "[shape]에 [mode]모드 설정하기", + "box2d@griffpatch.setPosition": "x: [x] y: [y] [space](으)로 이동하기", + "box2d@griffpatch.setProperties": "밀도 [density] 마찰력 [friction] 탄성력 [restitution](으)로 정하기", + "box2d@griffpatch.setRestitution": "탄성력을 [restitution](으)로 정하기", + "box2d@griffpatch.setRestitutionValue": "탄성력을 [restitution](으)로 정하기", + "box2d@griffpatch.setScroll": "화면 스크롤을 x: [ox] y: [oy](으)로 정하기", + "box2d@griffpatch.setStage": "무대 범위를 [stageType](으)로 정하기", + "box2d@griffpatch.setStatic": "[static]하기", + "box2d@griffpatch.setTickRate": "물리 연산 속도를 [rate]/s 으로 정하기", "box2d@griffpatch.setVelocity": "속도를 x:[sx] y:[sy] (으)로 정하기", "clipboard@_Clipboard": "클립보드", "clipboard@_clipboard": "클립보드", "clipboard@_copy to clipboard: [TEXT]": "클립보드에 복사하기: [TEXT]", "clipboard@_last pasted text": "방금 붙여넣은 텍스트", "clipboard@_reset clipboard": "클립보드 비우기", - "clipboard@_when something is copied": "복사되었을 때", - "clipboard@_when something is pasted": "붙여넣기 되었을 때", + "clipboard@_when something is copied": "복사가 발생했을 때", + "clipboard@_when something is pasted": "붙여넣기가 발생했을 때", "clouddata-ping@_Ping Cloud Data": "핑 클라우드 데이터", "clouddata-ping@_is cloud data server [SERVER] up?": "클라우드 데이터 서버 [SERVER]이(가) 활성화인가?", + "cloudlink@_A name": "어떤 이름", + "cloudlink@_Another name": "또다른 이름", + "cloudlink@_Hide old blocks": "구형 블록 숨기기", + "cloudlink@_Show old blocks": "구형 블록 보이기", "cloudlink@_[PATH] of [JSON_STRING]": "[JSON_STRING]에서 [PATH]", "cloudlink@_connected?": "연결되었는가?", + "cloudlink@_disconnect": "연결 끊기", + "cloudlink@_extension version": "확장 기능 버전", + "cloudlink@_failed to connnect?": "연결에 실패했는가?", + "cloudlink@_is [JSON_STRING] valid JSON?": "[JSON_STRING]이(가) 유효한 JSON인가?", + "cloudlink@_lost connection?": "연결이 끊겼는가?", "cloudlink@_my IP address": "내 IP 주소", + "cloudlink@_send [DATA]": "[DATA] 전송하기", + "cloudlink@_server list": "서버 목록", + "cloudlink@_server version": "서버 버전", + "cloudlink@_status code": "상태 코드", + "cloudlink@_username synced?": "사용자이름이 동기화됐는가?", + "cloudlink@_when I receive new [TYPE] message": "새 [TYPE] 메시지를 받았을 때", + "cloudlink@_when I receive new message with listener [ID]": "리스너 [ID]에서 새 메시지를 받았을 때", "cloudlink@_when connected": "연결되었을 때", "cloudlink@_when disconnected": "연결이 끊어졌을 때", "cs2627883/numericalencoding@_Hello!": "안녕!", "cs2627883/numericalencoding@_Numerical Encoding V1": "숫자 인코딩 V1", "cs2627883/numericalencoding@_decode [ENCODED] back to text": "[ENCODED]을(를) 텍스트로 디코딩", + "cs2627883/numericalencoding@_decoded": "디코딩 결과", "cs2627883/numericalencoding@_encode [DATA] to numbers": "[DATA]을(를) 숫자로 인코딩", - "cs2627883/numericalencoding@_encoded": "인코딩", + "cs2627883/numericalencoding@_encoded": "인코딩 결과", "cursor@_Mouse Cursor": "마우스 커서", "cursor@_bottom left": "왼쪽 아래", "cursor@_bottom right": "오른쪽 아래", @@ -4559,8 +4870,10 @@ "cursor@_cursor": "커서", "cursor@_hide cursor": "커서 숨기기", "cursor@_set cursor to [cur]": "커서를 [cur](으)로 정하기", + "cursor@_set cursor to current costume center: [position] max size: [size]": "커서를 현재 모양으로 정하기 중심: [position] 최대 크기: [size]", "cursor@_top left": "왼쪽 위", "cursor@_top right": "오른쪽 위", + "cursor@_{size} (unreliable)": "{size} (불안정)", "encoding@_Encoding": "인코딩", "encoding@_[string] corresponding to the [CodeList] character": "[string]에 대응되는 [CodeList] 문자로", "encoding@_convert the character [string] to [CodeList]": "문자 [string]을(를) [CodeList](으)로 변환", @@ -4576,10 +4889,14 @@ "files@_any": "아무거나", "files@_download URL [url] as [file]": "URL [url]에서 [file](으)로 다운로드하기", "files@_download [text] as [file]": "[text]을(를) [file](으)로 다운로드하기", + "files@_only show selector (unreliable)": "선택기만 열기 (불안정)", "files@_open a [extension] file": "[extension] 파일 열기", "files@_open a [extension] file as [as]": "[extension] 파일을 [as](으)로 열기", "files@_open a file": "파일 열기", "files@_open a file as [as]": "파일을 [as](으)로 열기", + "files@_open selector immediately": "선택기 열기", + "files@_set open file selector mode to [mode]": "파일 선택기 열기 방식을 [mode]로 정하기", + "files@_show modal": "선택 상자 보이기", "files@_text": "텍스트", "gamejolt@_1 point": "1 포인트", "gamejolt@_Close": "닫기", @@ -4632,7 +4949,7 @@ "gamejolt@_fetched table [tableDataType] at index[index] (Deprecated)": "불러온 테이블 [index]번째의 [tableDataType] (사용되지 않음)", "gamejolt@_fetched tables in JSON": "불러온 테이블 목록 JSON", "gamejolt@_fetched trophies in JSON": "불러온 트로피 목록 JSON", - "gamejolt@_fetched trophy [trophyDataType] at index [index]": "불러온 [trophyDataType] 트로피 [index]번째", + "gamejolt@_fetched trophy [trophyDataType] at index [index]": "불러온 트로피 [index]번째의 [trophyDataType]", "gamejolt@_fetched user's [userDataType]": "불러온 사용자의 [userDataType]", "gamejolt@_fetched user's data in JSON": "불러온 사용자 데이터 JSON", "gamejolt@_fetched user's friend ID at index[index]": "불러온 사용자의 친구 ID [index]번째", @@ -4679,7 +4996,7 @@ "gamejolt@_timestamp": "타임스탬프", "gamejolt@_timezone": "타임존", "gamejolt@_title": "제목", - "gamejolt@_turn debug mode [toggle]": "디버그 모드 [toggle]", + "gamejolt@_turn debug mode [toggle]": "디버그 모드를 [toggle]", "gamejolt@_update [globalOrPerUser] data at [key] by [operationType] [value]": "[globalOrPerUser] 데이터 [key]을(를) [value]만큼 [operationType]", "gamejolt@_user Blocks": "사용자 블록", "gamejolt@_user ID": "사용자 ID", @@ -4708,51 +5025,121 @@ "gamepad@_button [b] on pad [i] pressed?": "패드 [i]의 [b]버튼이 눌렸는가? ", "gamepad@_direction of axes [axis] on pad [pad]": "패드 [pad]의 [axis]축 방향", "gamepad@_gamepad [pad] connected?": "게임패드 [pad]이(가) 연결되었는가?", - "gamepad@_value of axis [b] on pad [i]": "패드 [i]의 [b]축 값 ", - "gamepad@_value of button [b] on pad [i]": "패드 [i]의 [b]버튼 값", + "gamepad@_magnitude of axes [axis] on pad [pad]": "패드 [pad]의 [axis]축 magnitude", + "gamepad@_rumble strong [s] and weak [w] for [t] sec. on pad [i]": "패드 [i]에서 강 [s] 약 [w]으로 [t]초동안 진동하기 ", + "gamepad@_set axis deadzone to [DEADZONE]": "축 데드존을 [DEADZONE](으)로 정하기", + "gamepad@_value of axis [b] on pad [i]": "패드 [i]의 [b]축 입력값 ", + "gamepad@_value of button [b] on pad [i]": "패드 [i]의 [b]버튼 입력값", + "godslayerakp/http@_Hide Extra": "나머지 숨기기", "godslayerakp/http@_Request": "요청", "godslayerakp/http@_Response": "응답", - "godslayerakp/http@_[name] from header": "헤더 [name]", + "godslayerakp/http@_Show Extra": "나머지 보이기", + "godslayerakp/http@_[name] from header": "헤더의 [name]", + "godslayerakp/http@_[path] in request options": "요청 options의 [path]", + "godslayerakp/http@_clear current data": "현재 데이터 지우기", "godslayerakp/http@_error": "오류", - "godslayerakp/http@_headers as json": "헤더를 json으로", + "godslayerakp/http@_headers as json": "헤더 json", "godslayerakp/http@_in header set [name] to [value]": "헤더의 [name]을(를) [value](으)로 정하기", "godslayerakp/http@_request failed?": "요청이 실패했는가?", "godslayerakp/http@_request succeeded?": "요청이 성공했는가?", "godslayerakp/http@_response": "응답", "godslayerakp/http@_send request to [url]": "[url](으)로 요청 보내기 ", - "godslayerakp/http@_set headers to json [json]": "헤더를 [json] json으로 정하기", - "godslayerakp/http@_set request method to [method]": "요청 메소드를 [method](으)로 정하기", + "godslayerakp/http@_set [path] to [value] in request options": "요청 options의 [path]을(를) [value](으)로 정하기", + "godslayerakp/http@_set [path] to type [type] in request options": "요청 options의 [path]을(를) [type]타입으로 정하기", + "godslayerakp/http@_set content type to [type]": "content type을 [type](으)로 정하기", + "godslayerakp/http@_set headers to json [json]": "헤더를 [json]인 json으로 정하기", + "godslayerakp/http@_set request body to [text]": "요청 body를 [text](으)로 정하기", + "godslayerakp/http@_set request method to [method]": "요청 메서드를 [method](으)로 정하기", "godslayerakp/http@_site responded?": "사이트가 응답했는가?", "godslayerakp/http@_status": "상태", "godslayerakp/http@_status text": "상태 메시지", + "godslayerakp/http@_type of [path] in request options": "요청 options의 [path]의 타입", "godslayerakp/http@_when a request fails": "요청이 실패했을 때", "godslayerakp/http@_when a site responds": "사이트가 응답했을 때", + "godslayerakp/ws@_close connection": "연결 닫기", + "godslayerakp/ws@_close connection with code [CODE]": "코드 [CODE](으)로 연결 닫기", + "godslayerakp/ws@_close connection with reason [REASON] and code [CODE]": "이유 [REASON] 코드 [CODE](으)로 연결 닫기", + "godslayerakp/ws@_closing code": "닫기 코드", + "godslayerakp/ws@_closing message": "닫기 메시지", "godslayerakp/ws@_connect to [URL]": "[URL]으로 연결하기", + "godslayerakp/ws@_connection errored?": "연결 오류가 발생했는가?", "godslayerakp/ws@_is connected?": "연결되었는가?", + "godslayerakp/ws@_is connection closed?": "연결이 닫혔는가?", "godslayerakp/ws@_received message data": "맏은 메시지 데이터", + "godslayerakp/ws@_send message [PAYLOAD]": "메시지 보내기 [PAYLOAD]", "godslayerakp/ws@_when connected": "연결되었을 때", + "godslayerakp/ws@_when connection closes": "연결이 닫혔을 때", + "godslayerakp/ws@_when connection errors": "연결 오류가 발생했을 때", "godslayerakp/ws@_when message received": "메시지를 받았을 때", - "iframe@_It works!": "잘 된다!", "iframe@_close iframe": "iframe 닫기", "iframe@_height": "높이", "iframe@_hide iframe": "iframe 숨기기", - "iframe@_scale": "크기", + "iframe@_iframe [MENU]": "iframe의 [MENU]", + "iframe@_interactive": "상호작용 여부", + "iframe@_resize behavior": "크기 조정 방식", "iframe@_set iframe height to [HEIGHT]": "iframe 높이를 [HEIGHT](으)로 정하기", + "iframe@_set iframe interactive to [INTERACTIVE]": "iframe 상호작용 여부를 [INTERACTIVE](으)로 정하기", + "iframe@_set iframe resize behavior to [RESIZE]": "iframe 크기 조정 방식을 [RESIZE](으)로 정하기", "iframe@_set iframe width to [WIDTH]": "iframe 넓이를 [WIDTH](으)로 정하기", "iframe@_set iframe x position to [X]": "iframe x좌표를 [X](으)로 정하기", "iframe@_set iframe y position to [Y]": "iframe y좌표를 [Y](으)로 정하기", "iframe@_show HTML [HTML]": "HTML [HTML] 보이기", "iframe@_show iframe": "iframe 보이기", "iframe@_show website [URL]": "웹사이트 [URL] 보이기", + "iframe@_visible": "보임 여부", "iframe@_width": "넓이", "itchio@_name": "이름", "itchio@_title": "제목", + "lab/text@_# of lines": "줄 수", + "lab/text@_# of lines [WITH_WORD_WRAP]": "[WITH_WORD_WRAP] 줄 수", + "lab/text@_Animated Text": "애니메이션 텍스트", + "lab/text@_Enable Non-Scratch Lab Features": "비 Scratch Lab 기능 활성화하기", "lab/text@_Hello!": "안녕!", + "lab/text@_Here we go!": "간다!", + "lab/text@_Incompatible with Scratch Lab:": "Scratch Lab과 호환되지 않음:", + "lab/text@_Welcome to my project!": "제 프로젝트에 오신걸 환영해요!", + "lab/text@_[ANIMATE] duration": "[ANIMATE]의 동작 시간", + "lab/text@_[ANIMATE] text [TEXT]": "[ANIMATE] 텍스트 [TEXT]", + "lab/text@_add line [TEXT]": "문구 추가하기 [TEXT]", + "lab/text@_align text to [ALIGN]": "텍스트를 [ALIGN] 정렬하기", + "lab/text@_animate [ANIMATE] until done": "[ANIMATE] 애니메이션 시작하고 기다리기", "lab/text@_center": "가운데", + "lab/text@_displayed text": "나타난 텍스트", + "lab/text@_is animating?": "애니메이션 중인가?", + "lab/text@_is showing text?": "텍스트가 나타났는가?", "lab/text@_left": "왼쪽", + "lab/text@_rainbow": "무지개", + "lab/text@_random font": "무작위 글꼴", + "lab/text@_reset [ANIMATE] duration": "[ANIMATE]의 동작 시간 초기화하기", + "lab/text@_reset text width": "텍스트 넓이를 초기화하기", + "lab/text@_reset typing delay": "타자 효과의 간격 초기화하기", "lab/text@_right": "오른쪽", + "lab/text@_set [ANIMATE] duration to [NUM] seconds": "[ANIMATE]의 동작 시간을 [NUM]초로 정하기", + "lab/text@_set font to [FONT]": "글꼴을 [FONT](으)로 정하기", + "lab/text@_set text color to [COLOR]": "텍스트 색을 [COLOR](으)로 정하기", + "lab/text@_set typing delay to [NUM] seconds": "타자 효과의 간격을 [NUM]초로 정하기", + "lab/text@_set width to [WIDTH]": "넓이를 [WIDTH](으)로 정하기", + "lab/text@_set width to [WIDTH] aligned [ALIGN]": "넓이 [WIDTH]의 [ALIGN] 정렬로 정하기", + "lab/text@_show sprite": "스프라이트로 보이기", + "lab/text@_show text [TEXT]": "텍스트 [TEXT] 보이기", + "lab/text@_start [ANIMATE] animation": "[ANIMATE] 애니메이션 시작하기", + "lab/text@_text [ATTRIBUTE]": "텍스트의 [ATTRIBUTE]", + "lab/text@_type": "타자", + "lab/text@_typing delay": "타자 효과의 간격", + "lab/text@_with word wrap": "줄바꿈을 포함한", + "lab/text@_without word wrap": "줄바꿈을 제외한", + "lab/text@_zoom": "커지기", + "lab/text@disableCompatibilityMode": "앞으로 추가되는 기등들은 공식 Scratch Lab과 호환되지 않습니다.\n\n계속하시겠습니까?", "local-storage@_Local Storage": "로컬 스토리지", + "local-storage@_Local Storage extension: project must run the \"set storage namespace ID\" block before it can use other blocks": "로컬 스토리지 확장 기능: 확장 기능의 블록을 사용하기 전에 반드시 \"스토리지의 네임스페이스 ID를 ...(으)로 정하기\" 블록을 실행해야 합니다", + "local-storage@_delete all keys": "모든 키 삭제하기", + "local-storage@_delete key [KEY]": "[KEY] 키 삭제하기", + "local-storage@_get key [KEY]": "[KEY] 키의 값", + "local-storage@_project title": "프로젝트 제목", "local-storage@_score": "점수", + "local-storage@_set key [KEY] to [VALUE]": "키 [KEY]을(를) [VALUE](으)로 정하기", + "local-storage@_set storage namespace ID to [ID]": "스토리지의 네임스페이스 ID를 [ID](으)로 정하기", + "local-storage@_when another window changes storage": "다른 창에서 스토리지를 변경했을 때", "mbw/xml@_add child [CHILD] to [XML]": "[XML]에 자식 [CHILD] 추가하기", "mbw/xml@_attribute [ATTR] of [XML]": "[XML]의 [ATTR]속성", "mbw/xml@_attributes of [XML]": "[XML] 속성", @@ -4760,75 +5147,135 @@ "mbw/xml@_children amount of [XML]": "[XML]의 자식 개수", "mbw/xml@_does [XML] have attribute [ATTR]?": "[XML]이(가) [ATTR]속성이 있는가?", "mbw/xml@_does [XML] have children?": "[XML]이(가) 자식이 있는가? ", + "mbw/xml@_error message of [MAYBE_XML]": "[MAYBE_XML] 오류 내용", + "mbw/xml@_inner elements of [XML]": "[XML]의 내부 요소", "mbw/xml@_is [MAYBE_XML] valid XML?": "[MAYBE_XML]이(가) 유효한 XML인가?", + "mbw/xml@_query [QUERY] on [XML]": "[XML]에 [QUERY] 쿼리로 검색", + "mbw/xml@_query [QUERY] on [XML] matches?": "[XML]에 [QUERY] 쿼리 검색 결과가 있는가?", + "mbw/xml@_query all [QUERY] on [XML]": "[XML]에 [QUERY] 쿼리로 모두 검색", "mbw/xml@_remove attribute [ATTR] of [XML]": "[XML]의 [ATTR]속성을 삭제하기", "mbw/xml@_remove child #[NO] of [XML]": "[XML]의 자식 [NO]번째를 삭제하기", "mbw/xml@_replace child #[NO] of [XML] with [CHILD]": "[XML]의 자식 [NO]번째를 [CHILD](으)로 바꾸기", "mbw/xml@_set attribute [ATTR] of [XML] to [VALUE]": "[XML]의 [ATTR]속성을 [VALUE](으)로 정하기", - "mbw/xml@_tag name of [XML]": "[XML] 태그 이름", + "mbw/xml@_set inner elements of [XML] to [VALUE]": "[XML]의 내부 요소를 [VALUE](으)로 정하기", + "mbw/xml@_set text of [XML] to [VALUE]": "[XML]의 텍스트를 [VALUE](으)로 정하기", + "mbw/xml@_tag name of [XML]": "[XML] 태그명", "mbw/xml@_text of [XML]": "[XML] 텍스트", "mdwalters/notifications@_Hello, world!": "헬로 월드!", "mdwalters/notifications@_Notification from project": "프로젝트의 알림", "mdwalters/notifications@_Notifications": "알림", "mdwalters/notifications@_close notification": "알림 닫기", - "mdwalters/notifications@_create notification with text [text]": "텍스트로 알림 생성하기 [text]", - "mdwalters/notifications@_has notification permission?": "알림 권한이 있는가?", + "mdwalters/notifications@_create notification with text [text]": "텍스트 알림 생성하기 [text]", + "mdwalters/notifications@_has notification permission?": "알림 권한을 받았는가?", "mdwalters/notifications@_request notification permission": "알림 권한 요청하기", "navigator@_Navigator Info": "네비게이터 정보", "navigator@_browser": "브라우저", + "navigator@_dark": "다크", + "navigator@_device memory in GB": "기기 메모리 GB", + "navigator@_light": "라이트", "navigator@_operating system": "운영 체제", + "navigator@_user prefers [THEME] color scheme?": "사용자가 [THEME]색상 테마를 선호하는가?", + "navigator@_user prefers more contrast?": "사용자가 고대비를 선호하는가?", + "navigator@_user prefers reduced motion?": "사용자가 감소된 움직임을 선호하는가?", + "numerical-encoding-2@_Hello": "안녕", "numerical-encoding-2@_Numerical Encoding V2": "숫자 인코딩 V2", "numerical-encoding-2@_decode [TEXT] as text": "[TEXT]을(를) 텍스트로 디코딩", "numerical-encoding-2@_encode [TEXT] as numbers": "[TEXT]을(를) 숫자로 인코딩", + "obviousAlexC/SensingPlus@_# of clones of [Sprite]": "[Sprite]의 복제본 개수", "obviousAlexC/SensingPlus@_# of fingers down": "입력된 손가락 개수", + "obviousAlexC/SensingPlus@_# of simultaneous possible": "최대 동시입력 개수", + "obviousAlexC/SensingPlus@_Needs a gyroscope or accelerometer": "자이로스코프 또는 속도계가 필요합니다", "obviousAlexC/SensingPlus@_No sprites exist": "(스프라이트 없음)", - "obviousAlexC/SensingPlus@_Sensing+": "감치 플러스", - "obviousAlexC/SensingPlus@_Touch blocks are broken in Safari.": "터지 블록은 Safari 브라우저에서 작동하지 않습니다.", + "obviousAlexC/SensingPlus@_Sensing+": "감지 플러스", + "obviousAlexC/SensingPlus@_Speech recording is unreliable": "음성 녹음 기능은 불안정합니다", + "obviousAlexC/SensingPlus@_accelerometer": "속도계", "obviousAlexC/SensingPlus@_brightness": "밝기", "obviousAlexC/SensingPlus@_color": "색깔", + "obviousAlexC/SensingPlus@_copied contents": "복사된 내용", + "obviousAlexC/SensingPlus@_does [List] contain [term]": "[List]이(가) [term]을(를) 포함하는가?", + "obviousAlexC/SensingPlus@_finger [ID] [PositionType]": "손가락 [ID]의 [PositionType]좌표 ", + "obviousAlexC/SensingPlus@_finger [ID] speed": "손가락 [ID]의 속도", "obviousAlexC/SensingPlus@_fisheye": "어안 렌즈", + "obviousAlexC/SensingPlus@_get index [index] of [List]": "[List]의 [index]번째", "obviousAlexC/SensingPlus@_ghost": "투명도", + "obviousAlexC/SensingPlus@_gyroscope": "자이로스코프", + "obviousAlexC/SensingPlus@_has a [device]?": "[device]가 있는가?", + "obviousAlexC/SensingPlus@_hidden?": "숨겨졌는가?", + "obviousAlexC/SensingPlus@_is finger [ID] down?": "손가락 [ID](이)가 입력중인가?", + "obviousAlexC/SensingPlus@_is packaged?": "패키징되었는가?", + "obviousAlexC/SensingPlus@_item # of [term] in [List]": "[List]에서 [term]의 번째", + "obviousAlexC/SensingPlus@_length of [List]": "[List]의 길이", "obviousAlexC/SensingPlus@_mosaic": "모자이크", "obviousAlexC/SensingPlus@_off": "끄기", "obviousAlexC/SensingPlus@_on": "켜기", "obviousAlexC/SensingPlus@_pixelate": "픽셀화", + "obviousAlexC/SensingPlus@_recognized Words": "감지된 말", + "obviousAlexC/SensingPlus@_recording?": "녹음중인가?", "obviousAlexC/SensingPlus@_rotation style": "회전 방식", + "obviousAlexC/SensingPlus@_set clipboard to [TEXT]": "클립보드에 [TEXT] 복사하기", + "obviousAlexC/SensingPlus@_sprite layer": "스프라이트 순서", "obviousAlexC/SensingPlus@_supports touches?": "터치를 지원하는가?", + "obviousAlexC/SensingPlus@_this sprite's [effect] effect": "이 스프라이트의 [effect] 효과", + "obviousAlexC/SensingPlus@_touching a clone of [Sprite]?": "[Sprite]의 복제본에 닿았는가?", + "obviousAlexC/SensingPlus@_touching the original [Sprite]?": "원본 [Sprite]에 닿았는가?", + "obviousAlexC/SensingPlus@_turn speech recording [toggle]": "음성 녹음 [toggle]하기", "obviousAlexC/SensingPlus@_whirl": "소용돌이", + "obviousAlexC/newgroundsIO@_API status": "API 상태", + "obviousAlexC/newgroundsIO@_All Time": "전체 기간", "obviousAlexC/newgroundsIO@_MedalID": "메달ID", + "obviousAlexC/newgroundsIO@_Today": "오늘", "obviousAlexC/newgroundsIO@_change game version to [version]": "게임 버전을 [version](으)로 바꾸기", "obviousAlexC/newgroundsIO@_currently logged in?": "로그인 되어있는가?", + "obviousAlexC/newgroundsIO@_formatted score": "포매팅된 점수", "obviousAlexC/newgroundsIO@_game version": "게임 버전", "obviousAlexC/newgroundsIO@_gameID": "게임ID", "obviousAlexC/newgroundsIO@_is medal [medalID] unlocked?": "메달 [medalID](이)가 잠금 해제되었는가?", + "obviousAlexC/newgroundsIO@_is user a newgrounds supporter?": "사용자가 newgrounds 서포터인가?", + "obviousAlexC/newgroundsIO@_profile picture": "프로필 사진", "obviousAlexC/newgroundsIO@_score": "점수", "obviousAlexC/newgroundsIO@_unlock medal [medalID]": "메달 [medalID] 잠금 해제하기", - "obviousAlexC/newgroundsIO@_user [datType]": "사용자 [datType]", + "obviousAlexC/newgroundsIO@_user [datType]": "사용자의 [datType]", "obviousAlexC/newgroundsIO@_username": "사용자 이름", "obviousAlexC/newgroundsIO@_when login required": "로그인이 필요할 때", "obviousAlexC/newgroundsIO@_when login success": "로그인에 성공했을 때", "obviousAlexC/penPlus@_Advanced": "고급 설정", + "obviousAlexC/penPlus@_Color": "색", + "obviousAlexC/penPlus@_Cubemaps": "큐브맵", "obviousAlexC/penPlus@_Custom Shaders": "사용자 정의 셰이더", "obviousAlexC/penPlus@_Extras": "그 외", "obviousAlexC/penPlus@_Height": "높이", "obviousAlexC/penPlus@_Images": "이미지", "obviousAlexC/penPlus@_Pen Properties": "펜 속성", + "obviousAlexC/penPlus@_Pen+ Costumes": "펜+ 모양", "obviousAlexC/penPlus@_Pen+ V7": "펜 플러스 V7", "obviousAlexC/penPlus@_Pen+ version": "펜 플러스 버전", "obviousAlexC/penPlus@_Shader Editor": "셰이더 편집기", + "obviousAlexC/penPlus@_Shader Manager": "셰이더 관리", "obviousAlexC/penPlus@_Square Pen Blocks": "사각형 블록", "obviousAlexC/penPlus@_Triangle Blocks": "삼각형 블록", "obviousAlexC/penPlus@_Width": "넓이", + "obviousAlexC/penPlus@_add blank image that is [color] and the size of [width], [height] named [name] to Pen+ Library": "펜+ 저장소에 색상 [color] 크기 [width], [height]의 빈 이미지 [name] 추가하기", + "obviousAlexC/penPlus@_add image named [name] from [dataURI] to Pen+ Library": "펜+ 저장소에 이미지 [name] 추가하기 [dataURI]", "obviousAlexC/penPlus@_brightness": "밝기", "obviousAlexC/penPlus@_clock-wise": "시계 방향", "obviousAlexC/penPlus@_color": "색깔", - "obviousAlexC/penPlus@_counter clock-wise": "시계 반대 방향", + "obviousAlexC/penPlus@_counter clock-wise": "반시계 방향", + "obviousAlexC/penPlus@_create cubemap named [name] from left [left] right [right] back [back] front [front] bottom [bottom] top [top]": "큐브맵 [name] 생성하기 왼쪽 [left] 오른쪽 [right] 뒤쪽 [back] 앞쪽 [front] 아래쪽 [bottom] 위쪽 [top]", + "obviousAlexC/penPlus@_data uri of pen layer": "펜 레이어 dataURI", + "obviousAlexC/penPlus@_does [name] exist as a cubemap": "큐브맵 [name]이(가) 존재하는가?", + "obviousAlexC/penPlus@_does [name] exist in Pen+ Library": "펜+ 저장소에 [name]이(가) 존재하는가?", "obviousAlexC/penPlus@_draw dot at [x] [y]": "점 [x] [y] 그리기", "obviousAlexC/penPlus@_draw line from [x1] [y1] to [x2] [y2]": "직선 [x1] [y1] 부터 [x2] [y2] 까지 그리기", + "obviousAlexC/penPlus@_draw square using [shader]": "[shader](으)로 사각형 그리기", "obviousAlexC/penPlus@_draw textured triangle between [x1] [y1], [x2] [y2] and [x3] [y3] with the texture [tex]": "텍스쳐 삼각형 그리기 [x1] [y1], [x2] [y2], [x3] [y3] 텍스쳐 [tex]", "obviousAlexC/penPlus@_draw triangle between [x1] [y1], [x2] [y2] and [x3] [y3]": "삼각형 그리기 [x1] [y1], [x2] [y2], [x3] [y3]", + "obviousAlexC/penPlus@_draw triangle using [shader] between [x1] [y1], [x2] [y2] and [x3] [y3]": "[shader](으)로 삼각형 [x1] [y1], [x2] [y2], [x3] [y3] 그리기", "obviousAlexC/penPlus@_get data uri for costume [costume]": "모양 [costume]의 dataURI", + "obviousAlexC/penPlus@_get data uri of [costume] in the pen+ costume library": "펜+ 저장소의 [costume]의 dataURI", + "obviousAlexC/penPlus@_get pen square's [target]": "펜 사각형의 [target] 값", "obviousAlexC/penPlus@_get pixel [x] [y]'s color in [costume]": "모양 [costume]의 [x] [y] 픽셀의 색상 ", + "obviousAlexC/penPlus@_get the [dimension] of [costume] in pen+ costume library": "펜+ 저장소의 [costume]의 [dimension]값", + "obviousAlexC/penPlus@_get triangle point [point]'s [attribute]": "삼각형 [point]번째 정점의 [attribute] 값", "obviousAlexC/penPlus@_get value of [component] in vector 2 [uniformName] in [shader]": "[shader]의 Vector2 [uniformName]의 [component]값", "obviousAlexC/penPlus@_get value of [component] in vector 3 [uniformName] in [shader]": "[shader]의 Vector3 [uniformName]의 [component]값", "obviousAlexC/penPlus@_get value of [component] in vector 4 [uniformName] in [shader]": "[shader]의 Vector4 [uniformName]의 [component]값", @@ -4838,20 +5285,35 @@ "obviousAlexC/penPlus@_hue [H] saturation [S] value [V]": "Hue[H] Saturation[S] Value[V]", "obviousAlexC/penPlus@_off": "끄기", "obviousAlexC/penPlus@_on": "켜기", + "obviousAlexC/penPlus@_pen [HSV]": "펜 [HSV]", "obviousAlexC/penPlus@_pen is down?": "펜을 내렸는가?", "obviousAlexC/penPlus@_red [R] green [G] blue [B]": "Red[R] Green[G] Blue[B]", + "obviousAlexC/penPlus@_remove cubemap named [name]": "큐브맵 [name]을(를) 제거하기", + "obviousAlexC/penPlus@_remove image named [name] from Pen+ Library": "펜+ 저장소의 이미지 [name]을(를) 제거하기", + "obviousAlexC/penPlus@_reset square Attributes": "사각형의 속성 초기화하기", + "obviousAlexC/penPlus@_reset triangle attributes": "삼각형의 속성 초기화하기", "obviousAlexC/penPlus@_saturation": "채도", + "obviousAlexC/penPlus@_set [setting] to [value]": "[setting]을(를) [value](으)로 정하기", + "obviousAlexC/penPlus@_set cubemap [uniformName] in [shader] to [cubemap]": "[shader]의 큐브맵 [uniformName]을(를) [cubemap](으)로 정하기", "obviousAlexC/penPlus@_set matrix [uniformName] in [shader] to [array]": "[shader]의 행렬 [uniformName]을(를) [array](으)로 정하기", "obviousAlexC/penPlus@_set matrix [uniformName] in [shader] to [list]": "[shader]의 행렬 [uniformName]을(를) [list](으)로 정하기", + "obviousAlexC/penPlus@_set pen square's [target] to [number]": "펜 사각형의 [target]을(를) [number](으)로 정하기", "obviousAlexC/penPlus@_set pixel [x] [y]'s color to [color] in [costume]": "모양 [costume]의 [x] [y] 픽셀의 색상을 [color](으)로 정하기", "obviousAlexC/penPlus@_set texture [uniformName] in [shader] to [texture]": "[shader]의 텍스쳐 [uniformName]을(를) [texture](으)로 정하기", + "obviousAlexC/penPlus@_set the prefix for [prefix] to [value]": "[prefix]의 접두사를 [value](으)로 정하기", + "obviousAlexC/penPlus@_set triangle point [point]'s [attribute] to [value]": "삼각형 [point]번째 정점의 [attribute]을(를) [value](으)로 정하기", + "obviousAlexC/penPlus@_set triangle's [wholeAttribute] to [value]": "삼각형의 [wholeAttribute]을(를) [value](으)로 정하기", "obviousAlexC/penPlus@_set vector 2 [uniformName] in [shader] to [numberX] [numberY]": "[shader]의 Vector2 [uniformName]을(를) [numberX] [numberY] (으)로 정하기", "obviousAlexC/penPlus@_set vector 3 [uniformName] in [shader] to [numberX] [numberY] [numberZ]": "[shader]의 Vector3 [uniformName]을(를) [numberX] [numberY] [numberZ] (으)로 정하기", "obviousAlexC/penPlus@_set vector 4 [uniformName] in [shader] to [numberX] [numberY] [numberZ] [numberW]": "[shader]의 Vector4 [uniformName]을(를) [numberX] [numberY] [numberZ] [numberW] (으)로 정하기", + "obviousAlexC/penPlus@_shaders in project": "프로젝트의 셰이더 목록", "obviousAlexC/penPlus@_size": "크기", "obviousAlexC/penPlus@_stamp [sprite]": "[sprite] 도장찍기", "obviousAlexC/penPlus@_stamp pen square": "펜 사각형 도장찍기", - "obviousAlexC/penPlus@_transparency": "투명", + "obviousAlexC/penPlus@_stamp pen square with the texture of [tex]": "펜 사각형을 [tex] 텍스쳐로 도장찍기", + "obviousAlexC/penPlus@_transparency": "투명도", + "obviousAlexC/penPlus@_triangles drawn": "그려진 삼각형 수", + "obviousAlexC/penPlus@_turn advanced setting [Setting] [onOrOff]": "고급 설정인 [Setting] [onOrOff]", "obviousAlexC/penPlus@_width": "넓이", "pointerlock@_Pointerlock": "포인터 잠금", "pointerlock@_disabled": "비활성화", @@ -4874,28 +5336,43 @@ "qxsck/var-and-list@getList": "[LIST] 값", "qxsck/var-and-list@getValueOfList": "[LIST]의 [INDEX]번째", "qxsck/var-and-list@getVar": "[VAR] 값", + "qxsck/var-and-list@length": "[LIST]의 길이", "qxsck/var-and-list@listContains": "[LIST]이(가) [VALUE]을(를) 포함하는가?", "qxsck/var-and-list@name": "변수 및 리스트", "qxsck/var-and-list@replaceOfList": "[LIST]의 [INDEX]번째를 [VALUE](으)로 정하기", + "qxsck/var-and-list@seriListsToJson": "[START](으)로 시작하는 모든 리스트 json", + "qxsck/var-and-list@seriVarsToJson": "[START](으)로 시작하는 모든 변수 json", "qxsck/var-and-list@setVar": "[VAR]을(를) [VALUE](으)로 정하기", "rixxyx@_RixxyX is cool, right?": "RixxyX 참 좋죠?", + "rixxyx@_[BOOL] as boolean": "[BOOL]을(를) 불리언으로", + "rixxyx@_[NUM] as number": "[NUM]을(를) 숫자로", + "rixxyx@_[TEXT] as text": "[TEXT]을(를) 텍스트로", "rixxyx@_[TEXT] to lowercase": "소문자 [TEXT]", "rixxyx@_[TEXT] to uppercase": "대문자 [TEXT]", "rixxyx@_[TEXT_1] is the same type as [TEXT_2]?": "[TEXT_1]이(가) [TEXT_2]와(과) 같은 타입인가?", + "rixxyx@_binary [BIN] to text": "바이너리 [BIN]을(를) 텍스트로", + "rixxyx@_capitalize [TEXT]": "첫 대문자 [TEXT]", "rixxyx@_color [COLOR] in hex": "색상 [COLOR]의 Hex코드", "rixxyx@_counter": "카운터", + "rixxyx@_decrement counter by [NUM]": "카운터를 [NUM]만큼 빼기", + "rixxyx@_end measuring time": "시간 측정 끝내기", + "rixxyx@_extract text [TEXT] between [NUM_1] to [NUM_2] characters": "텍스트 [TEXT]의 [NUM_1]부터 [NUM_2]까지를 추출", "rixxyx@_false": "거짓", "rixxyx@_if [BOOL] then [TEXT]": "만약 [BOOL](이)라면 [TEXT]", "rixxyx@_if [BOOL] then [TEXT_1] else [TEXT_2]": "만약 [BOOL](이)라면 [TEXT_1] 아니면 [TEXT_2] ", + "rixxyx@_increment counter by [NUM]": "카운터를 [NUM]만큼 더하기", "rixxyx@_is javascript NaN [OBJ]": "[OBJ]이(가) javascript NaN 인가?", "rixxyx@_repeat text [TEXT] [NUM] times": "텍스트 [TEXT](을)를 [NUM]번 반복", "rixxyx@_reverse text [TEXT]": "역방향 텍스트 [TEXT]", "rixxyx@_rixxyX is cool, right?": "RixxyX 참 좋죠?", "rixxyx@_set counter to [NUM]": "카운터를 [NUM](으)로 정하기", + "rixxyx@_start measuring time": "시간 측정 시작하기", + "rixxyx@_text [TEXT] to binary": "텍스트 [TEXT]을(를) 바이너리로", + "rixxyx@_time": "시간", "rixxyx@_true": "참", "runtime-options@_Infinity": "무제한", "runtime-options@_Runtime Options": "실행 설정", - "runtime-options@_[thing] enabled?": "[thing]이(가) 활성화 되었는가?", + "runtime-options@_[thing] enabled?": "[thing]이(가) 활성화인가?", "runtime-options@_clone limit": "복제본 개수 제한", "runtime-options@_default ({n})": "기본 ({n})", "runtime-options@_disabled": "비활성화", @@ -4923,18 +5400,19 @@ "shreder95ua/resolution@_primary screen height": "주 화면 높이", "shreder95ua/resolution@_primary screen width": "주 화면 넓이", "sound@_Sound": "소리", - "sound@_play sound from url: [path] until done": "URL에서 소리 끝까지 재생하기: [path]", + "sound@_play sound from url: [path] until done": "URL에서 소리 재생하고 기다리기: [path]", "sound@_start sound from url: [path]": "URL에서 소리 재생하기: [path]", "steamworks@_IP country": "IP 국가", "steamworks@_[TYPE] [ID] installed?": "[TYPE] [ID](이)가 설치되었는가?", "steamworks@_achievement [ACHIEVEMENT] unlocked?": "업적 [ACHIEVEMENT]이(가) 달성되었는가? ", - "steamworks@_false": "거짓", - "steamworks@_get user [THING]": "사용자 [THING] 얻기 ", + "steamworks@_false": "미달성함", + "steamworks@_get user [THING]": "사용자의 [THING] 값", "steamworks@_level": "레벨", "steamworks@_name": "이름", - "steamworks@_set achievement [ACHIEVEMENT] unlocked to [STATUS]": "업적 [ACHIEVEMENT] 달성을 [STATUS](으)로 정하기", + "steamworks@_open [TYPE] [DATA] in overlay": "[TYPE][DATA]을(를) 오버레이로 열기", + "steamworks@_set achievement [ACHIEVEMENT] unlocked to [STATUS]": "업적 [ACHIEVEMENT]을(를) [STATUS](으)로 정하기", "steamworks@_steam ID": "스팀 ID", - "steamworks@_true": "참", + "steamworks@_true": "달성함", "stretch@_Stretch": "늘리기", "stretch@_change stretch by x: [DX] y: [DY]": "늘리기를 x:[DX] y:[DY] 만큼 바꾸기", "stretch@_change stretch x by [DX]": "늘리기 x를 [DX]만큼 바꾸기", @@ -4956,6 +5434,8 @@ "text@_index of [SUBSTRING] in [STRING]": "[STRING]에서 [SUBSTRING]의 번째", "text@_is [OPERAND1] identical to [OPERAND2]?": "[OPERAND1]이(가) [OPERAND2]와(과) 정확히 같지 않은가?", "text@_is [STRING] [TEXTCASE]?": "[STRING]이(가) [TEXTCASE]인가?", + "text@_item [ITEM] of [STRING] matched by regex /[REGEX]/[FLAGS]": "[STRING]에서 정규표현식 /[REGEX]/[FLAGS] 의 [ITEM]번째 결과", + "text@_item [ITEM] of [STRING] split by [SPLIT]": "[STRING]을(를) [SPLIT](으)로 나눈 것의 [ITEM]번째", "text@_letters [LETTER1] to [LETTER2] of [STRING]": "[STRING]의 [START]부터 [END]까지의 글자", "text@_lowercase": "소문자 표기법 (abc)", "text@_repeat [STRING] [REPEAT] times": "[STRING] 문자열 [REPEAT]번 반복", @@ -4976,21 +5456,33 @@ "true-fantom/math@_Math": "수학", "true-fantom/math@_[A] exactly contains [B]?": "[A]이(가) [B]을(를) 정확히 포함하는가?", "true-fantom/math@_[A] is multiple of [B]?": "[A]이(가) [B]의 배수인가?", + "true-fantom/math@_clamp [A] between [B] and [C]": "[A] 값을 [B]부터 [C]까지 범위로 제한", + "true-fantom/math@_is float [A]?": "[A]이(가) 부동소수점인가?", "true-fantom/math@_is int [A]?": "[A]이(가) 정수인가?", "true-fantom/math@_is number [A]?": "[A](이)가 숫자인가?", "true-fantom/math@_is safe number [A]?": "[A]이(가) 안전한 숫자인가?", "true-fantom/math@_log of [A] with base [B]": "밑이 [B]인 log [A]", + "true-fantom/math@_map [A] from range [m1] - [M1] to range [m2] - [M2]": "[A] 값의 [m1] - [M1] 범위를 [m2] - [M2] 범위에 대응", + "true-fantom/math@_trunc of [A]": "버림 [A]", + "true-fantom/math@_trunc of [A] with [B] digits after dot": "[A]을(를) 소숫점 [B]자리 이후로 버림", "true-fantom/network@_(1) text": "(1) 텍스트", "true-fantom/network@_(3) status ok?": "(3) 상태가 ok인지 여부", - "true-fantom/network@_(7) redirected?": "(7) 리다이렉트인지 여부", + "true-fantom/network@_(4) status": "(4) 상태 코드", + "true-fantom/network@_(5 1) status text and text": "(5 1) 상태 메시지 및 텍스트", + "true-fantom/network@_(5) status text": "(5) 상태 메시지", + "true-fantom/network@_(7) redirected?": "(7) 리다이렉트 여부", + "true-fantom/network@_(9) body used?": "(9) body 사용 여부", "true-fantom/network@_Network": "네트워크", "true-fantom/network@_browser": "브라우저", "true-fantom/network@_connected to internet?": "인터넷에 연결되었는가?", "true-fantom/network@_current url": "현재 URL", "true-fantom/network@_default": "기본", + "true-fantom/network@_network generation": "네트워크 세대", "true-fantom/regexp@_RegExp": "정규표현식", "true-fantom/regexp@_[A] matches with [IMAGE] [B] ?": "[IMAGE] [B]에 [A]이(가) 일치하는가?", "true-fantom/regexp@_[B] of [IMAGE] [A]": "[IMAGE] [A]의 [B]", + "true-fantom/regexp@_[IMAGE2] [A] split by matches with [IMAGE1] [B]": "[IMAGE2] [A]을(를) [IMAGE1] [B]에 따라 나누기", + "true-fantom/regexp@_[IMAGE2] match [C] of [A] with [IMAGE1] [B]": "[IMAGE2] [A]에서 [IMAGE1] [B](으)로 일치하는 결과의 [C]", "true-fantom/regexp@_[IMAGE] [A] contains flags [B] ?": "[IMAGE] [A]이(가) 플래그 [B]을(를) 포함하는가?", "true-fantom/regexp@_[IMAGE] add flags [B] to [IMAGE] [A]": "[IMAGE] [A]에 플래그 [B] 추가하기", "true-fantom/regexp@_[IMAGE] delete flags [B] of [IMAGE] [A]": "[IMAGE] [A]의 플래그 [B] 삭제하기", @@ -5000,12 +5492,13 @@ "true-fantom/regexp@_is [IMAGE] [A] ?": "[IMAGE] [A]이(가) 정규표현식 인가?", "true-fantom/regexp@_keys": "키", "true-fantom/regexp@_map": "맵", - "true-fantom/regexp@_pairs": "쌍", + "true-fantom/regexp@_pairs": "페어", "true-fantom/regexp@_pattern": "패턴", "true-fantom/regexp@_replace matches of [A] with [IMAGE] [B] to [C]": "[IMAGE] [B](으)로 [A]에 일치하는 요소를 [C](으)로 바꾸기", "true-fantom/regexp@_values": "값", "utilities@_Utilities": "유틸리티", "utilities@_[PATH] of [JSON_STRING]": "[JSON_STRING]에서 [PATH]", + "utilities@_clamp [INPUT] between [MIN] and [MAX]": "[INPUT] 값을 [MIN]부터 [MAX]까지 범위로 제한", "utilities@_content from [URL]": "[URL]의 내용", "utilities@_current millisecond": "현재 밀리초", "utilities@_false": "거짓", @@ -5014,6 +5507,8 @@ "utilities@_letters [START] to [END] of [STRING]": "[STRING]의 [START]부터 [END]까지의 글자", "utilities@_true": "참", "veggiecan/LongmanDictionary@_Longman Dictionary": "Longman 사전", + "veggiecan/LongmanDictionary@_all definitions of [word] in the longman dictionary": "Longman 사전에서 [word]의 모든 정의", + "veggiecan/LongmanDictionary@_primary definition of [word] in the longman dictionary": "Longman 사전에서 [word]의 첫번째 정의", "veggiecan/browserfullscreen@_Browser Fullscreen": "브라우저 전체화면", "veggiecan/browserfullscreen@_[ACTION] fullscreen": "전체화면 [ACTION]", "veggiecan/browserfullscreen@_enter": "진입하기", @@ -5023,7 +5518,33 @@ "veggiecan/browserfullscreen@_in browser fullscreen?": "브라우저가 전체화면인가?", "veggiecan/browserfullscreen@_when browser fullscreen [ENABLED]": "브라우저가 전체화면을 [ENABLED]", "veggiecan/mobilekeyboard@_Mobile Keyboard": "모바일 키보드", - "vercte/dictionaries@_Dictionaries": "사전" + "veggiecan/mobilekeyboard@_Now the text is different": "이제 텍스트 내용이 달라집니다", + "veggiecan/mobilekeyboard@_You typed: ": "입력한 내용:", + "veggiecan/mobilekeyboard@_alphabetical": "영문", + "veggiecan/mobilekeyboard@_alphabetical (allows newlines)": "영문 (줄바꿈 포함)", + "veggiecan/mobilekeyboard@_close keyboard": "키보드 닫기", + "veggiecan/mobilekeyboard@_email adress": "이메일 주소", + "veggiecan/mobilekeyboard@_is any text selected?": "텍스트를 선택했는가?", + "veggiecan/mobilekeyboard@_is keyboard open?": "키보드가 열려있는가?", + "veggiecan/mobilekeyboard@_numerical": "숫자", + "veggiecan/mobilekeyboard@_search": "검색", + "veggiecan/mobilekeyboard@_set cursor position to [INDEX]": "커서 위치를 [INDEX](으)로 정하기", + "veggiecan/mobilekeyboard@_set text box's default value to [VALUE]": "입력 상자의 기본 내용을 [VALUE](으)로 정하기", + "veggiecan/mobilekeyboard@_set textbox current value to [TEXT]": "입력 상자의 내용을 [TEXT](으)로 정하기", + "veggiecan/mobilekeyboard@_show [TYPE] keyboard": "[TYPE] 키보드 열기", + "veggiecan/mobilekeyboard@_show [TYPE] keyboard and wait": "[TYPE] 키보드 열고 기다리기", + "veggiecan/mobilekeyboard@_typed text": "입력한 텍스트", + "veggiecan/mobilekeyboard@_web address": "웹 주소", + "vercte/dictionaries@_Dictionaries": "사전", + "vercte/dictionaries@_change key [KEY] in dictionary [DICT] by [BY]": "사전 [DICT]의 키 [KEY]을(를) [BY](으)로 변경하기", + "vercte/dictionaries@_key [KEY] from dictionary [DICT]": "사전 [DICT]의 키 [KEY]", + "vercte/dictionaries@_key [KEY] in dictionary [DICT] is defined?": "사전 [DICT]의 키 [KEY]이(가) 정의되었는가?", + "vercte/dictionaries@_key [KEY] in dictionary [DICT] is null?": "사전 [DICT]의 키 [KEY]이(가) null인가?", + "vercte/dictionaries@_parse JSON [OBJ] into dictionary [DICT]": "JSON [OBJ]을 사전 [DICT](으)로 변환", + "vercte/dictionaries@_remove dictionary [DICT]": "사전 [DICT]을(를) 제거하기", + "vercte/dictionaries@_remove key [KEY] from dictionary [DICT]": "사전 [DICT]의 키 [KEY]을(를) 제거하기", + "vercte/dictionaries@_set key [KEY] in dictionary [DICT] to [VAL]": "사전 [DICT]의 키 [KEY]을(를) [VAL](으)로 정하기", + "vercte/dictionaries@_stringify dictionary [DICT] into JSON": "사전 [DICT]을(를) JSON으로 문자열화" }, "lt": { "-SIPC-/consoles@_Error": "Klaida", @@ -5641,8 +6162,6 @@ "obviousAlexC/SensingPlus@_No local lists in other sprites": "Ingen lokale lister i andre sprites.", "obviousAlexC/SensingPlus@_No sprites exist": "Ingen sprites eksisterer", "obviousAlexC/SensingPlus@_Speech recording is unreliable": "Taleopptak er upålitelig.", - "obviousAlexC/SensingPlus@_Touch blocks are broken in Safari.": "Berøringssperrene fungerer ikke i Safari.", - "obviousAlexC/SensingPlus@_We will try to fix them soon.": "Vi vil prøve å fikse dem snart.", "obviousAlexC/SensingPlus@_accelerometer": "akselerometer", "obviousAlexC/SensingPlus@_brightness": "lysstyrke", "obviousAlexC/SensingPlus@_color": "farge", @@ -5663,11 +6182,9 @@ "obviousAlexC/SensingPlus@_off": "av", "obviousAlexC/SensingPlus@_on": "på", "obviousAlexC/SensingPlus@_pixelate": "pixelere", - "obviousAlexC/SensingPlus@_positional": "posisjonell", "obviousAlexC/SensingPlus@_recognized Words": "gjenkjente ord", "obviousAlexC/SensingPlus@_recording?": "opptaker?", "obviousAlexC/SensingPlus@_rotation style": "rotasjonsstil", - "obviousAlexC/SensingPlus@_rotational": "rotasjonell", "obviousAlexC/SensingPlus@_set clipboard to [TEXT]": "sett utklippstavlen til [TEXT]", "obviousAlexC/SensingPlus@_sprite layer": "sprite lag", "obviousAlexC/SensingPlus@_touching a clone of [Sprite]?": "berøre en kopi av [Sprite]?", @@ -5756,6 +6273,7 @@ "stretch@_x stretch": "x strekk", "stretch@_y stretch": "y strekk", "text@_Text": "Tekst", + "text@_end": "slutt", "true-fantom/base@_[A] from base [B] to base [C]": "[A] fra base [B] til base [C]", "true-fantom/base@_is base [B] [A]?": "er base [B] [A]?", "true-fantom/couplers@_Couplers": "Koblinger", @@ -6842,8 +7360,6 @@ "obviousAlexC/SensingPlus@_No sprites exist": "...", "obviousAlexC/SensingPlus@_Sensing+": "Waarnemen+", "obviousAlexC/SensingPlus@_Speech recording is unreliable": "Spraakopname is onbetrouwbaar", - "obviousAlexC/SensingPlus@_Touch blocks are broken in Safari.": "Aanraakblokken werken niet in Safari.", - "obviousAlexC/SensingPlus@_We will try to fix them soon.": "We doen ons best om het op te lossen.", "obviousAlexC/SensingPlus@_accelerometer": "versnellingsmeter", "obviousAlexC/SensingPlus@_brightness": "helderheid", "obviousAlexC/SensingPlus@_color": "kleur", @@ -6865,11 +7381,9 @@ "obviousAlexC/SensingPlus@_off": "uit", "obviousAlexC/SensingPlus@_on": "aan", "obviousAlexC/SensingPlus@_pixelate": "pixeleren", - "obviousAlexC/SensingPlus@_positional": "positionele", "obviousAlexC/SensingPlus@_recognized Words": "herkende woorden", "obviousAlexC/SensingPlus@_recording?": "aan het opnemen?", "obviousAlexC/SensingPlus@_rotation style": "draaistijl", - "obviousAlexC/SensingPlus@_rotational": "draai", "obviousAlexC/SensingPlus@_set clipboard to [TEXT]": "maak klembord [TEXT]", "obviousAlexC/SensingPlus@_sprite layer": "laag van sprite", "obviousAlexC/SensingPlus@_touching a clone of [Sprite]?": "raak ik een kloon van [Sprite]?", @@ -6984,6 +7498,7 @@ "text@_Title Case": "Alles Met Beginhoofdletter", "text@_UPPERCASE": "HOOFDLETTERS", "text@_[STRING] matches regex /[REGEX]/[FLAGS]?": "[STRING] komt overeen met regex /[REGEX]/[FLAGS]?", + "text@_apple": "appel", "text@_convert [STRING] to [TEXTCASE]": "zet [STRING] om naar [TEXTCASE]", "text@_count [SUBSTRING] in [STRING]": "aantal [SUBSTRING] in [STRING]", "text@_count regex /[REGEX]/[FLAGS] in [STRING]": "aantal overeenkomsten van regex /[REGEX]/[FLAGS] met [STRING]", @@ -7722,8 +8237,11 @@ "Lily/Video@_loaded videos": "загруженные видео", "Lily/Video@_pause video [NAME]": "приостановить видео [NAME]", "Lily/Video@_paused": "приостановлено", + "Lily/Video@_playback rate": "скорость воспроизведения", "Lily/Video@_playing": "играет", "Lily/Video@_resume video [NAME]": "продолжить видео [NAME]", + "Lily/Video@_screenshot of video [NAME] at current time": "снимок экрана видео [NAME] на текущий момент", + "Lily/Video@_set playback rate of video [NAME] to [RATE]": "задать скорость воспроизведения видео [NAME] значение [RATE]", "Lily/Video@_set volume of video [NAME] to [VALUE]": "задать звук видео [NAME] на [VALUE]", "Lily/Video@_show video [NAME] on [TARGET]": "показать видео [NAME] на[TARGET]", "Lily/Video@_start video [NAME] at [DURATION] seconds": "начать видео [NAME] на [DURATION] секундах", @@ -8455,10 +8973,8 @@ "godslayerakp/http@_Response": "Ответ", "godslayerakp/http@_Show Extra": "Показать Дополнительное", "godslayerakp/http@_[name] from header": "[name] из заголовка", - "godslayerakp/http@_[name] in request form": "[name] в форме запроса", "godslayerakp/http@_[path] in request options": "[path] в настройках запроса", "godslayerakp/http@_clear current data": "отчистить текущие данные", - "godslayerakp/http@_delete [name] from request form": "удалить [name] из формы запроса", "godslayerakp/http@_error": "ошибка", "godslayerakp/http@_headers as json": "заголовки как json", "godslayerakp/http@_in header set [name] to [value]": "в заголовке задать [name] на [value]", @@ -8466,13 +8982,11 @@ "godslayerakp/http@_request succeeded?": "запрос успешен?", "godslayerakp/http@_response": "ответ", "godslayerakp/http@_send request to [url]": "отправить запрос на [url]", - "godslayerakp/http@_set [name] to [value] in request form": "задать [name] на [value] в форме запроса", "godslayerakp/http@_set [path] to [value] in request options": "задать [path] на [value] в настройках запроса", "godslayerakp/http@_set [path] to type [type] in request options": "задать [path] на тип [type] в настройках запроса", "godslayerakp/http@_set content type to [type]": "задать тип контента на [type]", "godslayerakp/http@_set headers to json [json]": "задать заголовки на json [json]", "godslayerakp/http@_set request body to [text]": "задать тело запроса на [text]", - "godslayerakp/http@_set request body to a form": "задать тело запроса на форму", "godslayerakp/http@_set request method to [method]": "задать метод запроса на [method]", "godslayerakp/http@_site responded?": "сайт ответил?", "godslayerakp/http@_status": "статус", @@ -8609,6 +9123,7 @@ "mbw/xml@_does [XML] have attribute [ATTR]?": "[XML] имеет атрибут [ATTR]?", "mbw/xml@_does [XML] have children?": "[XML] имеет ребёнка?", "mbw/xml@_error message of [MAYBE_XML]": "ошибка сообщения [MAYBE_XML]", + "mbw/xml@_inner elements of [XML]": "внутренние элементы [XML]", "mbw/xml@_is [MAYBE_XML] valid XML?": "[MAYBE_XML] правильный XML файл?", "mbw/xml@_query [QUERY] on [XML]": "запрос [QUERY] на [XML]", "mbw/xml@_query [QUERY] on [XML] matches?": "запрос [QUERY] на [XML] совпадает?", @@ -8617,6 +9132,7 @@ "mbw/xml@_remove child #[NO] of [XML]": "удалить #[NO] ребёнка файла [XML]", "mbw/xml@_replace child #[NO] of [XML] with [CHILD]": "заменить ребёнка под номером #[NO] файла [XML] на [CHILD]", "mbw/xml@_set attribute [ATTR] of [XML] to [VALUE]": "задать атрибут [ATTR] XML файла [XML] в [VALUE]", + "mbw/xml@_set inner elements of [XML] to [VALUE]": "задать внутренним элементам [XML] значение [VALUE]", "mbw/xml@_set text of [XML] to [VALUE]": "задать текст [XML] на [VALUE]", "mbw/xml@_tag name of [XML]": "имя тега [XML]", "mbw/xml@_text of [XML]": "текст [XML]", @@ -8648,9 +9164,6 @@ "obviousAlexC/SensingPlus@_No sprites exist": "Никаких спрайтов не существует", "obviousAlexC/SensingPlus@_Sensing+": "Сенсоры+", "obviousAlexC/SensingPlus@_Speech recording is unreliable": "Запись речи ненадежна", - "obviousAlexC/SensingPlus@_Touch blocks are broken in Safari.": "Блоки дотрагивания сломаны в Safari.", - "obviousAlexC/SensingPlus@_We will try to fix them soon.": "Мы попробуем это починить в скором времени.", - "obviousAlexC/SensingPlus@_[type] speed on the [axis] axis": "[type] скорость на оси [axis]", "obviousAlexC/SensingPlus@_accelerometer": "акселерометр", "obviousAlexC/SensingPlus@_brightness": "яркость", "obviousAlexC/SensingPlus@_color": "цвет", @@ -8673,11 +9186,9 @@ "obviousAlexC/SensingPlus@_off": "Выключить", "obviousAlexC/SensingPlus@_on": "Включить", "obviousAlexC/SensingPlus@_pixelate": "укрупнение пикселей", - "obviousAlexC/SensingPlus@_positional": "позиционный", "obviousAlexC/SensingPlus@_recognized Words": "запомнищийся Cлова", "obviousAlexC/SensingPlus@_recording?": "запись?", "obviousAlexC/SensingPlus@_rotation style": "стиль поворота", - "obviousAlexC/SensingPlus@_rotational": "поворачиваемый", "obviousAlexC/SensingPlus@_set clipboard to [TEXT]": "задать буфер обмена в [TEXT]", "obviousAlexC/SensingPlus@_sprite layer": "слой спрайта", "obviousAlexC/SensingPlus@_supports touches?": "Поддерживает дотрагивания?", @@ -8978,9 +9489,11 @@ "text@_Title Case": "Тайтл Кейс", "text@_UPPERCASE": "ВЕРХНИЙ РЕГИСТР", "text@_[STRING] matches regex /[REGEX]/[FLAGS]?": "[STRING] совпадает с regex'ом /[REGEX]/[FLAGS]?", + "text@_apple": "яблоко", "text@_convert [STRING] to [TEXTCASE]": "сконвертировать строку [STRING] в [TEXTCASE]", "text@_count [SUBSTRING] in [STRING]": "количество [SUBSTRING] в [STRING]", "text@_count regex /[REGEX]/[FLAGS] in [STRING]": "посчитать regex /[REGEX]/[FLAGS] в [STRING]", + "text@_end": "конец", "text@_index of [SUBSTRING] in [STRING]": "индекс [SUBSTRING] в [STRING]", "text@_is [OPERAND1] identical to [OPERAND2]?": "[OPERAND1] идентичный с [OPERAND2]?", "text@_is [STRING] [TEXTCASE]?": "строка [STRING] это [TEXTCASE]?", @@ -9120,10 +9633,12 @@ "sl": { "-SIPC-/consoles@_Error": "Napaka", "Skyhigh173/json@_Advanced": "Napredno", + "files@_Files": "Datoteke", "files@_Select or drop file": "Izberite ali povlecite datoteko", "gamejolt@_Close": "Zapri", "obviousAlexC/penPlus@_Advanced": "Napredno", - "runtime-options@_Runtime Options": "Možnosti izvajanja" + "runtime-options@_Runtime Options": "Možnosti izvajanja", + "text@_Text": "Besedilo" }, "sr": { "Skyhigh173/json@_Advanced": "Напредно", @@ -10054,8 +10569,11 @@ "Lily/Video@_loaded videos": "已加载的视频", "Lily/Video@_pause video [NAME]": "暂停视频[NAME]", "Lily/Video@_paused": "暂停", + "Lily/Video@_playback rate": "播放速度", "Lily/Video@_playing": "播放", "Lily/Video@_resume video [NAME]": "继续视频[NAME]", + "Lily/Video@_screenshot of video [NAME] at current time": "视频[NAME]当前时间的截图", + "Lily/Video@_set playback rate of video [NAME] to [RATE]": "设置视频[NAME]的播放速度为[RATE]", "Lily/Video@_set volume of video [NAME] to [VALUE]": "将视频[NAME]的音量设为[VALUE]", "Lily/Video@_show video [NAME] on [TARGET]": "在[TARGET]上显示视频[NAME]", "Lily/Video@_start video [NAME] at [DURATION] seconds": "从第[DURATION]秒开始播放视频[NAME]", @@ -10785,10 +11303,8 @@ "godslayerakp/http@_Response": "响应", "godslayerakp/http@_Show Extra": "显示更多", "godslayerakp/http@_[name] from header": "请求头的[name]", - "godslayerakp/http@_[name] in request form": "请求表单中的[name]", "godslayerakp/http@_[path] in request options": "请求选项的[path]", "godslayerakp/http@_clear current data": "清空当前数据", - "godslayerakp/http@_delete [name] from request form": "从请求表单删除[name]", "godslayerakp/http@_error": "错误", "godslayerakp/http@_headers as json": "请求头json", "godslayerakp/http@_in header set [name] to [value]": "在请求头中设置[name]为[value]", @@ -10796,13 +11312,11 @@ "godslayerakp/http@_request succeeded?": "请求成功?", "godslayerakp/http@_response": "响应", "godslayerakp/http@_send request to [url]": "发送请求给[url]", - "godslayerakp/http@_set [name] to [value] in request form": "设置请求表单中的[name]为[value]", "godslayerakp/http@_set [path] to [value] in request options": "将请求选项中的[path]设为[value]", "godslayerakp/http@_set [path] to type [type] in request options": "将请求选项的[path]设为类型[type]", "godslayerakp/http@_set content type to [type]": "设置内容类型为[type]", "godslayerakp/http@_set headers to json [json]": "设置请求头为json[json]", "godslayerakp/http@_set request body to [text]": "设置请求体为[text]", - "godslayerakp/http@_set request body to a form": "设置请求体为表单", "godslayerakp/http@_set request method to [method]": "设置请求方法为[method]", "godslayerakp/http@_site responded?": "网站响应?", "godslayerakp/http@_status": "封禁状态", @@ -10942,6 +11456,7 @@ "mbw/xml@_does [XML] have attribute [ATTR]?": "[XML]有属性[ATTR]吗?", "mbw/xml@_does [XML] have children?": "[XML]有子元素吗?", "mbw/xml@_error message of [MAYBE_XML]": "[MAYBE_XML]的错误信息", + "mbw/xml@_inner elements of [XML]": "[XML]的内部元素", "mbw/xml@_is [MAYBE_XML] valid XML?": "[MAYBE_XML]是合法 XML?", "mbw/xml@_query [QUERY] on [XML]": "[XML]中第一个匹配[QUERY]的元素", "mbw/xml@_query [QUERY] on [XML] matches?": "[XML]能匹配[QUERY]吗?", @@ -10950,6 +11465,7 @@ "mbw/xml@_remove child #[NO] of [XML]": "删除[XML]第[NO]个子元素", "mbw/xml@_replace child #[NO] of [XML] with [CHILD]": "将[XML]第[NO]子元素替换为[CHILD]", "mbw/xml@_set attribute [ATTR] of [XML] to [VALUE]": "设置[XML]的属性[ATTR]为[VALUE]", + "mbw/xml@_set inner elements of [XML] to [VALUE]": "设置[XML]的内部元素为[VALUE]", "mbw/xml@_set text of [XML] to [VALUE]": "设置[XML]的文字为[VALUE]", "mbw/xml@_tag name of [XML]": "[XML]的标签名称", "mbw/xml@_text of [XML]": "[XML]的文本", @@ -10980,9 +11496,6 @@ "obviousAlexC/SensingPlus@_No sprites exist": "没有角色", "obviousAlexC/SensingPlus@_Sensing+": "侦测 +", "obviousAlexC/SensingPlus@_Speech recording is unreliable": "语音录制是不可靠的", - "obviousAlexC/SensingPlus@_Touch blocks are broken in Safari.": "触碰积木在 Safari 可能无法使用。", - "obviousAlexC/SensingPlus@_We will try to fix them soon.": "以后会尝试修复。", - "obviousAlexC/SensingPlus@_[type] speed on the [axis] axis": "[axis]轴上的[type]速度", "obviousAlexC/SensingPlus@_accelerometer": "加速度计", "obviousAlexC/SensingPlus@_brightness": "亮度", "obviousAlexC/SensingPlus@_color": "颜色", @@ -11005,11 +11518,9 @@ "obviousAlexC/SensingPlus@_off": "关闭", "obviousAlexC/SensingPlus@_on": "打开", "obviousAlexC/SensingPlus@_pixelate": "像素化", - "obviousAlexC/SensingPlus@_positional": "移动", "obviousAlexC/SensingPlus@_recognized Words": "语音识别的文本", "obviousAlexC/SensingPlus@_recording?": "正在识别?", "obviousAlexC/SensingPlus@_rotation style": "旋转模式", - "obviousAlexC/SensingPlus@_rotational": "旋转", "obviousAlexC/SensingPlus@_set clipboard to [TEXT]": "设置剪贴板为[TEXT]", "obviousAlexC/SensingPlus@_sprite layer": "角色的图层", "obviousAlexC/SensingPlus@_supports touches?": "支持触碰?", @@ -11311,9 +11822,11 @@ "text@_Title Case": "标题", "text@_UPPERCASE": "大写", "text@_[STRING] matches regex /[REGEX]/[FLAGS]?": "[STRING] 满足正则表达式 /[REGEX]/[FLAGS]?", + "text@_apple": "苹果", "text@_convert [STRING] to [TEXTCASE]": "转换[STRING]为[TEXTCASE]", "text@_count [SUBSTRING] in [STRING]": "[STRING]中[SUBSTRING]的数量", "text@_count regex /[REGEX]/[FLAGS] in [STRING]": "使用正则表达式 /[REGEX]/[FLAGS] 在 [STRING] 匹配的数量", + "text@_end": "End", "text@_index of [SUBSTRING] in [STRING]": "[STRING]中[SUBSTRING]的位置", "text@_is [OPERAND1] identical to [OPERAND2]?": "[OPERAND1]===[OPERAND2]", "text@_is [STRING] [TEXTCASE]?": "[STRING]是[TEXTCASE]?",