├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .prettierrc ├── .prism-languages ├── LICENSE ├── README.md ├── assets ├── syzoj-applogo.svg └── syzoj-favicon.svg ├── index.html ├── package.json ├── patches ├── @vitejs+plugin-legacy+2.0.0.patch ├── fomantic-ui-css+2.8.8.patch ├── markdown-it-mentions+1.0.0.patch ├── react-navi+0.15.0.patch ├── rollup-plugin-node-polyfills+0.2.1.patch ├── vite-plugin-html-minifier-terser+2.0.3.patch └── vite-plugin-public-path+1.0.0.patch ├── public ├── favicon.ico └── robots.txt ├── scripts └── generate-api.js ├── src ├── App.tsx ├── AppRouter.tsx ├── api-generated │ ├── index.ts │ ├── modules │ │ ├── auth.ts │ │ ├── discussion.ts │ │ ├── group.ts │ │ ├── homepage.ts │ │ ├── judge-client.ts │ │ ├── migration.ts │ │ ├── problem.ts │ │ ├── submission.ts │ │ └── user.ts │ └── types.d.ts ├── api.ts ├── appState.ts ├── assets │ ├── default-avatar.svg │ ├── monaco-themes │ │ ├── tomorrow-night.json │ │ └── tomorrow.json │ ├── prism-tomorrow-night.css │ └── prism-tomorrow.css ├── components │ ├── CodeBox.module.less │ ├── CodeBox.tsx │ ├── CodeEditor.module.less │ ├── CodeEditor.tsx │ ├── CodeLanguageAndOptions.tsx │ ├── DiscussionSearch.tsx │ ├── EmojiRenderer.module.less │ ├── EmojiRenderer.tsx │ ├── GlobalProgressBar.tsx │ ├── HorizontalScroll.module.less │ ├── HorizontalScroll.tsx │ ├── LazyCodeEditor.tsx │ ├── LazyPermissionManager.tsx │ ├── LocalizeTab.module.less │ ├── LocalizeTab.tsx │ ├── Pagination.module.less │ ├── Pagination.tsx │ ├── PermissionManager.module.less │ ├── PermissionManager.tsx │ ├── PreviewSearch.module.less │ ├── PreviewSearch.tsx │ ├── ProblemSearch.tsx │ ├── PseudoLink.tsx │ ├── ScoreText.module.less │ ├── ScoreText.tsx │ ├── StatusText.module.less │ ├── StatusText.tsx │ ├── TableCellSearchDropdown.module.less │ ├── TableCellSearchDropdown.tsx │ ├── TimeAgo.tsx │ ├── UserAvatar.tsx │ ├── UserLink.tsx │ ├── UserSearch.module.less │ └── UserSearch.tsx ├── index.less ├── index.ts ├── index.tsx ├── initApp.ts ├── interfaces │ ├── CodeLanguage.ts │ ├── GroupMeta.ts │ ├── Locale.ts │ ├── ProblemType.ts │ ├── SubmissionStatus.ts │ └── UserMeta.ts ├── layouts │ ├── AppLayout.module.less │ └── AppLayout.tsx ├── locales │ ├── index.ts │ ├── messages │ │ ├── en-US.ts │ │ ├── en-US │ │ │ ├── code_language.js │ │ │ ├── common.js │ │ │ ├── components │ │ │ │ ├── code_box.js │ │ │ │ ├── permission_manager.js │ │ │ │ ├── problem_search.js │ │ │ │ └── user_search.js │ │ │ ├── discussion.js │ │ │ ├── discussion_edit.js │ │ │ ├── discussions.js │ │ │ ├── error.js │ │ │ ├── forgot.js │ │ │ ├── groups.js │ │ │ ├── home.js │ │ │ ├── home_settings.js │ │ │ ├── import.ts │ │ │ ├── judge_machine.js │ │ │ ├── language.js │ │ │ ├── login.js │ │ │ ├── problem.js │ │ │ ├── problem_edit.js │ │ │ ├── problem_files.js │ │ │ ├── problem_judge_settings.js │ │ │ ├── problem_set.js │ │ │ ├── problem_tag_manager.js │ │ │ ├── register.js │ │ │ ├── submission.js │ │ │ ├── submission_item.js │ │ │ ├── submission_statistics.js │ │ │ ├── submissions.js │ │ │ ├── user.js │ │ │ ├── user_audit.js │ │ │ ├── user_edit.js │ │ │ └── users.js │ │ ├── importMessages.ts │ │ ├── ja-JP.ts │ │ ├── ja-JP │ │ │ ├── code_language.js │ │ │ ├── common.js │ │ │ ├── components │ │ │ │ ├── code_box.js │ │ │ │ ├── permission_manager.js │ │ │ │ ├── problem_search.js │ │ │ │ └── user_search.js │ │ │ ├── discussion.js │ │ │ ├── discussion_edit.js │ │ │ ├── discussions.js │ │ │ ├── error.js │ │ │ ├── forgot.js │ │ │ ├── groups.js │ │ │ ├── home.js │ │ │ ├── home_settings.js │ │ │ ├── import.ts │ │ │ ├── judge_machine.js │ │ │ ├── language.js │ │ │ ├── login.js │ │ │ ├── problem.js │ │ │ ├── problem_edit.js │ │ │ ├── problem_files.js │ │ │ ├── problem_judge_settings.js │ │ │ ├── problem_set.js │ │ │ ├── problem_tag_manager.js │ │ │ ├── register.js │ │ │ ├── submission.js │ │ │ ├── submission_item.js │ │ │ ├── submission_statistics.js │ │ │ ├── submissions.js │ │ │ ├── user.js │ │ │ ├── user_audit.js │ │ │ ├── user_edit.js │ │ │ └── users.js │ │ ├── zh-CN.ts │ │ └── zh-CN │ │ │ ├── code_language.js │ │ │ ├── common.js │ │ │ ├── components │ │ │ ├── code_box.js │ │ │ ├── permission_manager.js │ │ │ ├── problem_search.js │ │ │ └── user_search.js │ │ │ ├── discussion.js │ │ │ ├── discussion_edit.js │ │ │ ├── discussions.js │ │ │ ├── error.js │ │ │ ├── forgot.js │ │ │ ├── groups.js │ │ │ ├── home.js │ │ │ ├── home_settings.js │ │ │ ├── import.ts │ │ │ ├── judge_machine.js │ │ │ ├── language.js │ │ │ ├── login.js │ │ │ ├── problem.js │ │ │ ├── problem_edit.js │ │ │ ├── problem_files.js │ │ │ ├── problem_judge_settings.js │ │ │ ├── problem_set.js │ │ │ ├── problem_tag_manager.js │ │ │ ├── register.js │ │ │ ├── submission.js │ │ │ ├── submission_item.js │ │ │ ├── submission_statistics.js │ │ │ ├── submissions.js │ │ │ ├── user.js │ │ │ ├── user_audit.js │ │ │ ├── user_edit.js │ │ │ └── users.js │ └── meta.ts ├── markdown │ ├── MarkdownContent.module.less │ ├── MarkdownContent.tsx │ ├── markdown.ts │ ├── mathjax.ts │ └── sanitize.ts ├── misc │ ├── analytics.js │ └── fonts │ │ ├── index.ts │ │ └── ui-font-selectors.js ├── pages │ ├── auth │ │ ├── common.module.less │ │ ├── forgot │ │ │ ├── ForgotPage.tsx │ │ │ └── index.ts │ │ ├── login │ │ │ ├── LoginPage.tsx │ │ │ └── index.ts │ │ └── register │ │ │ ├── RegisterPage.tsx │ │ │ └── index.ts │ ├── discussion │ │ ├── discussions │ │ │ ├── DiscussionsPage.module.less │ │ │ └── DiscussionsPage.tsx │ │ ├── edit │ │ │ ├── DiscussionEditPage.module.less │ │ │ └── DiscussionEditPage.tsx │ │ ├── index.ts │ │ ├── utils.ts │ │ └── view │ │ │ ├── DiscussionViewPage.module.less │ │ │ ├── DiscussionViewPage.tsx │ │ │ └── LoadMoreBackground.svg │ ├── error │ │ ├── ErrorPage.module.less │ │ └── ErrorPage.tsx │ ├── home │ │ ├── home-settings │ │ │ ├── HomeSettingsPage.module.less │ │ │ └── HomeSettingsPage.tsx │ │ ├── home │ │ │ ├── HomePage.module.less │ │ │ └── HomePage.tsx │ │ └── index.ts │ ├── judge-machine │ │ ├── JudgeMachinePage.module.less │ │ ├── JudgeMachinePage.tsx │ │ └── index.ts │ ├── problem │ │ ├── edit │ │ │ ├── ProblemEditPage.module.less │ │ │ ├── ProblemEditPage.tsx │ │ │ └── defaultSections.ts │ │ ├── files │ │ │ ├── ProblemFilesPage.module.less │ │ │ └── ProblemFilesPage.tsx │ │ ├── index.ts │ │ ├── judge-settings │ │ │ ├── ProblemJudgeSettingsPage.module.less │ │ │ ├── ProblemJudgeSettingsPage.tsx │ │ │ ├── common │ │ │ │ ├── CheckerEditor.module.less │ │ │ │ ├── CheckerEditor.tsx │ │ │ │ ├── ExtraSourceFilesEditor.module.less │ │ │ │ ├── ExtraSourceFilesEditor.tsx │ │ │ │ ├── MetaEditor.module.less │ │ │ │ ├── MetaEditor.tsx │ │ │ │ ├── SubtasksEditor.module.less │ │ │ │ ├── SubtasksEditor.tsx │ │ │ │ ├── TestDataFileSelector.module.less │ │ │ │ ├── TestDataFileSelector.tsx │ │ │ │ ├── common.module.less │ │ │ │ ├── detect-testcases.ts │ │ │ │ └── interface.ts │ │ │ └── types │ │ │ │ ├── InteractionProblemEditor.module.less │ │ │ │ ├── InteractionProblemEditor.tsx │ │ │ │ ├── SubmitAnswerProblemEditor.tsx │ │ │ │ └── TraditionalProblemEditor.tsx │ │ ├── problem-set │ │ │ ├── ProblemSetPage.module.less │ │ │ ├── ProblemSetPage.tsx │ │ │ ├── ProblemTagManager.module.less │ │ │ └── ProblemTagManager.tsx │ │ ├── problemTag.ts │ │ ├── utils.ts │ │ └── view │ │ │ ├── ProblemViewPage.module.less │ │ │ ├── ProblemViewPage.tsx │ │ │ ├── common │ │ │ ├── CodeLanguageAndOptions.tsx │ │ │ ├── FileChooser.module.less │ │ │ ├── FileChooser.tsx │ │ │ ├── SubmitViewFrame.module.less │ │ │ ├── SubmitViewFrame.tsx │ │ │ ├── TabbedEditor.module.less │ │ │ ├── TabbedEditor.tsx │ │ │ ├── common.module.less │ │ │ ├── index.ts │ │ │ └── interface.ts │ │ │ └── types │ │ │ ├── InteractionProblemView.tsx │ │ │ ├── SubmitAnswerProblemView.tsx │ │ │ ├── TraditionalProblemView.module.less │ │ │ └── TraditionalProblemView.tsx │ ├── submission │ │ ├── common.ts │ │ ├── componments │ │ │ ├── SubmissionItem.module.less │ │ │ └── SubmissionItem.tsx │ │ ├── index.ts │ │ ├── statistics │ │ │ ├── SubmissionStatisticsPage.module.less │ │ │ └── SubmissionStatisticsPage.tsx │ │ ├── submission │ │ │ ├── SubmissionPage.module.less │ │ │ ├── SubmissionPage.tsx │ │ │ ├── common │ │ │ │ ├── FormattableCodeBox.module.less │ │ │ │ ├── FormattableCodeBox.tsx │ │ │ │ └── interface.ts │ │ │ └── types │ │ │ │ ├── InteractionProblemSubmissionView.tsx │ │ │ │ ├── SubmitAnswerProblemSubmissionView.tsx │ │ │ │ └── TraditionalProblemSubmissionView.tsx │ │ └── submissions │ │ │ ├── SubmissionsPage.module.less │ │ │ └── SubmissionsPage.tsx │ └── user │ │ ├── edit │ │ ├── AuditView.tsx │ │ ├── PreferenceView.tsx │ │ ├── PrivilegeView.tsx │ │ ├── ProfileView.tsx │ │ ├── SecurityView.tsx │ │ ├── UserEdit.module.less │ │ └── UserEditPage.tsx │ │ ├── groups │ │ ├── GroupsPage.module.less │ │ └── GroupsPage.tsx │ │ ├── index.ts │ │ ├── user │ │ ├── UserPage.module.less │ │ └── UserPage.tsx │ │ └── users │ │ ├── UsersPage.module.less │ │ └── UsersPage.tsx ├── themes │ ├── index.ts │ ├── prism-theme.ts │ └── themes │ │ ├── far.css │ │ ├── far.ts │ │ ├── pure.css │ │ └── pure.ts ├── utils │ ├── CodeFormatter.ts │ ├── CodeHighlighter.ts │ ├── callApiWithFileUpload.ts │ ├── copyToClipboard.ts │ ├── createWorker.ts │ ├── downloadFile.ts │ ├── fixChineseSpace.ts │ ├── formatDateTime.tsx │ ├── formatFileSize.ts │ ├── getFileIcon.ts │ ├── getRoute.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useAsyncCallbackPending.ts │ │ ├── useBoundingRect.ts │ │ ├── useDialog.tsx │ │ ├── useFieldCheck.tsx │ │ ├── useFieldCheckSimple.ts │ │ ├── useFocusWithin.ts │ │ ├── useLocalizer.ts │ │ ├── useLoginOrRegisterNavigation.ts │ │ ├── useMaybeAsyncFunctionResult.ts │ │ ├── useNavigation.ts │ │ ├── useRecaptcha.tsx │ │ ├── useScreenWidthWithin.ts │ │ └── useSocket.ts │ ├── onEnterPress.ts │ ├── openUploadDialog.ts │ ├── pipeStream.ts │ ├── readFile.ts │ ├── toast.ts │ ├── validators.ts │ └── zip.ts └── vite-env.d.ts ├── tsconfig.json ├── vite.config.ts ├── vite └── svgo.ts └── yarn.lock /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out 12 | uses: actions/checkout@v2 13 | - name: Set up Node 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: 16.x 17 | - name: Install dependencies 18 | run: yarn --frozen-lockfile 19 | - name: Build 20 | run: yarn build 21 | env: 22 | NODE_OPTIONS: --max_old_space_size=8192 23 | - name: Upload Build Artifact 24 | uses: actions/upload-artifact@v2 25 | with: 26 | path: dist 27 | - name: Check style 28 | run: yarn check-style 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish & Deploy 2 | 3 | on: 4 | push: 5 | tags: v* 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out 12 | uses: actions/checkout@v2 13 | - name: Set up Node 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: 16.x 17 | - name: Install dependencies 18 | run: yarn --frozen-lockfile 19 | - name: Check style 20 | run: yarn check-style 21 | - name: Build 22 | run: yarn build 23 | env: 24 | NODE_OPTIONS: --max_old_space_size=8192 25 | - name: Publish 26 | uses: JS-DevTools/npm-publish@v1 27 | with: 28 | token: ${{ secrets.NPM_TOKEN }} 29 | tag: ${{ endsWith(github.ref_name, 'next') && 'next' || 'latest' }} 30 | - name: Start Deployment for LibreOJ CDN 31 | uses: octokit/request-action@v2.x 32 | with: 33 | route: POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches 34 | owner: LibreOJ 35 | repo: CDN 36 | workflow_id: deploy.yaml 37 | ref: main 38 | inputs: "{}" 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.CDN_DEPLOY_GITHUB_TOKEN }} 41 | - name: Start Deployment for LibreOJ Bootstrap 42 | run: | 43 | eval `ssh-agent -s` 44 | echo "$SSH_KEY" | tr -d '\r' | ssh-add - 45 | 46 | git config --global user.name $(git show -s --format='%an' HEAD) 47 | git config --global user.email $(git show -s --format='%ae' HEAD) 48 | 49 | git clone git@github.com:libreoj/bootstrap libreoj-bootstrap --depth 1 50 | cd libreoj-bootstrap 51 | jq "{ version: .version }" ../package.json > version.json 52 | git commit -am "Update $(jq -r ".name" ../package.json) to $(jq -r ".version" ../package.json)" 53 | git push 54 | env: 55 | SSH_KEY: ${{ secrets.DEPLOY_KEY }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | /stats.html 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /dist 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | .vscode 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "printWidth": 120, 4 | "arrowParens": "avoid", 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /.prism-languages: -------------------------------------------------------------------------------- 1 | markup 2 | css 3 | clike 4 | javascript 5 | actionscript 6 | ada 7 | agda 8 | antlr4 9 | applescript 10 | arduino 11 | asciidoc 12 | aspnet 13 | autohotkey 14 | bash 15 | basic 16 | batch 17 | bbcode 18 | bison 19 | bnf 20 | brainfuck 21 | c 22 | csharp 23 | cpp 24 | clojure 25 | cmake 26 | d 27 | dart 28 | diff 29 | docker 30 | ebnf 31 | editorconfig 32 | ejs 33 | elixir 34 | erlang 35 | fsharp 36 | fortran 37 | git 38 | go 39 | graphql 40 | groovy 41 | haml 42 | haskell 43 | http 44 | ini 45 | java 46 | jq 47 | json 48 | json5 49 | julia 50 | kotlin 51 | latex 52 | less 53 | lisp 54 | llvm 55 | lua 56 | makefile 57 | markdown 58 | matlab 59 | monkey 60 | nasm 61 | nginx 62 | nix 63 | objectivec 64 | ocaml 65 | pascal 66 | perl 67 | php 68 | powershell 69 | protobuf 70 | python 71 | r 72 | racket 73 | jsx 74 | tsx 75 | reason 76 | regex 77 | ruby 78 | rust 79 | sass 80 | scss 81 | scala 82 | scheme 83 | smali 84 | sparql 85 | sql 86 | stylus 87 | swift 88 | tcl 89 | textile 90 | toml 91 | typescript 92 | vala 93 | vbnet 94 | verilog 95 | vhdl 96 | vim 97 | basic 98 | wasm 99 | yaml 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Menci 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/syzoj-applogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/syzoj-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /patches/fomantic-ui-css+2.8.8.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/fomantic-ui-css/components/site.css b/node_modules/fomantic-ui-css/components/site.css 2 | index b9e10c4..6ff4f19 100644 3 | --- a/node_modules/fomantic-ui-css/components/site.css 4 | +++ b/node_modules/fomantic-ui-css/components/site.css 5 | @@ -13,7 +13,6 @@ 6 | Page 7 | *******************************/ 8 | 9 | -@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400%3B0,700%3B1,400%3B1,700&subset=latin&display=swap'); 10 | html, 11 | body { 12 | height: 100%; 13 | -------------------------------------------------------------------------------- /patches/markdown-it-mentions+1.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/markdown-it-mentions/index.js b/node_modules/markdown-it-mentions/index.js 2 | index a89b364..5dbdf41 100644 3 | --- a/node_modules/markdown-it-mentions/index.js 4 | +++ b/node_modules/markdown-it-mentions/index.js 5 | @@ -22,4 +22,4 @@ function parser(match, utils) { 6 | ].join(''); 7 | } 8 | 9 | -module.exports = regexp(/@(\w+)/, parser); 10 | +module.exports = regexp(/@([a-zA-Z0-9\-_.#$]{3,24})/, parser); 11 | -------------------------------------------------------------------------------- /patches/react-navi+0.15.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-navi/dist/es/Link.js b/node_modules/react-navi/dist/es/Link.js 2 | index 206c99d..b2899d4 100644 3 | --- a/node_modules/react-navi/dist/es/Link.js 4 | +++ b/node_modules/react-navi/dist/es/Link.js 5 | @@ -78,9 +78,9 @@ export var useLinkProps = function (_a) { 6 | "This value is no longer supported - please set it to \"mount\" instead."); 7 | } 8 | } 9 | - // Prefetch on hover by default. 10 | + // Prefetch disabled by default. 11 | if (prefetch === undefined) { 12 | - prefetch = 'hover'; 13 | + prefetch = false; 14 | } 15 | var hashScrollBehaviorFromContext = React.useContext(HashScrollContext); 16 | var context = React.useContext(NaviContext); 17 | -------------------------------------------------------------------------------- /patches/rollup-plugin-node-polyfills+0.2.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/rollup-plugin-node-polyfills/dist/types/index.d.ts b/node_modules/rollup-plugin-node-polyfills/dist/types/index.d.ts 2 | index fdc8d97..5dbd529 100644 3 | --- a/node_modules/rollup-plugin-node-polyfills/dist/types/index.d.ts 4 | +++ b/node_modules/rollup-plugin-node-polyfills/dist/types/index.d.ts 5 | @@ -1,7 +1,7 @@ 6 | import { NodePolyfillsOptions } from './modules'; 7 | export default function (opts?: NodePolyfillsOptions): { 8 | name: string; 9 | - resolveId(importee: string, importer: string): { 10 | + resolveId(importee: string, importer: string | undefined): { 11 | id: any; 12 | moduleSideEffects: boolean; 13 | } | null; 14 | -------------------------------------------------------------------------------- /patches/vite-plugin-html-minifier-terser+2.0.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/vite-plugin-html-minifier-terser/index.d.ts b/node_modules/vite-plugin-html-minifier-terser/index.d.ts 2 | index 6433335..62b82ba 100644 3 | --- a/node_modules/vite-plugin-html-minifier-terser/index.d.ts 4 | +++ b/node_modules/vite-plugin-html-minifier-terser/index.d.ts 5 | @@ -1,4 +1,4 @@ 6 | -import { Plugin } from 'vite'; 7 | +import { Plugin } from '../vite'; 8 | interface VitePluginHtmlMinifierTerser { 9 | removeAttributeQuotes?: boolean, 10 | collapseWhitespace?: boolean, 11 | -------------------------------------------------------------------------------- /patches/vite-plugin-public-path+1.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/vite-plugin-public-path/dist/processing/html.js b/node_modules/vite-plugin-public-path/dist/processing/html.js 2 | index 66c437c..d3b9a13 100644 3 | --- a/node_modules/vite-plugin-public-path/dist/processing/html.js 4 | +++ b/node_modules/vite-plugin-public-path/dist/processing/html.js 5 | @@ -88,6 +88,11 @@ function processHtml(config, options, _fileName, html) { 6 | const patchAttributes = ["src", "data-src"]; 7 | const excludeScriptsFilter = options.excludeScripts ? (0, pluginutils_1.createFilter)(options.excludeScripts) : () => false; 8 | const scriptTags = document.querySelectorAll("script[src], script[nomodule]").filter(tag => { 9 | + if (tag.hasAttribute("data-notransform")) { 10 | + tag.removeAttribute("data-notransform"); 11 | + console.log("!!!"); 12 | + return false; 13 | + } 14 | var _a; 15 | if (!patchAttributes.some(attr => tag.hasAttribute(attr))) { 16 | // script tag has no src or data-src, keep tag for rewrite. 17 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyrio-dev/ui/945cd11270e51ed178999fe59ee13cd0d82fce73/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { observer } from "mobx-react"; 3 | import { HelmetProvider, Helmet } from "react-helmet-async"; 4 | 5 | import { appState } from "./appState"; 6 | import { updateCodeFontCss, updateContentFontCss, updateUiFontCss } from "./misc/fonts"; 7 | import { setGlobalTheme } from "./themes"; 8 | 9 | import AppRouter from "./AppRouter"; 10 | 11 | const App: React.FC = () => { 12 | useEffect(() => { 13 | updateCodeFontCss(appState.locale); 14 | updateContentFontCss(appState.locale); 15 | updateUiFontCss(appState.locale); 16 | }, [appState.userPreference.font, appState.locale]); 17 | 18 | useEffect(() => { 19 | setGlobalTheme(appState.theme); 20 | }, [appState.theme]); 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 30 | 31 | {appState.title && `${appState.title} - `} 32 | {appState.serverPreference.siteName} 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default observer(App); 42 | -------------------------------------------------------------------------------- /src/api-generated/index.ts: -------------------------------------------------------------------------------- 1 | // This file is generated automatically, do NOT modify it. 2 | 3 | import * as ImportedUserApi from "./modules/user"; 4 | import * as ImportedAuthApi from "./modules/auth"; 5 | import * as ImportedSubmissionApi from "./modules/submission"; 6 | import * as ImportedProblemApi from "./modules/problem"; 7 | import * as ImportedMigrationApi from "./modules/migration"; 8 | import * as ImportedGroupApi from "./modules/group"; 9 | import * as ImportedJudgeClientApi from "./modules/judge-client"; 10 | import * as ImportedDiscussionApi from "./modules/discussion"; 11 | import * as ImportedHomepageApi from "./modules/homepage"; 12 | 13 | export const user = ImportedUserApi; 14 | export const auth = ImportedAuthApi; 15 | export const submission = ImportedSubmissionApi; 16 | export const problem = ImportedProblemApi; 17 | export const migration = ImportedMigrationApi; 18 | export const group = ImportedGroupApi; 19 | export const judgeClient = ImportedJudgeClientApi; 20 | export const discussion = ImportedDiscussionApi; 21 | export const homepage = ImportedHomepageApi; 22 | -------------------------------------------------------------------------------- /src/api-generated/modules/auth.ts: -------------------------------------------------------------------------------- 1 | // This file is generated automatically, do NOT modify it. 2 | 3 | /// 4 | 5 | import { createGetApi, createPostApi } from "@/api"; 6 | 7 | export const getSessionInfo = createGetApi<{ token?: string; jsonp?: string }, ApiTypes.GetSessionInfoResponseDto>( 8 | "auth/getSessionInfo" 9 | ); 10 | export const login = createPostApi("auth/login", true); 11 | export const logout = createPostApi("auth/logout", false); 12 | export const checkAvailability = createGetApi< 13 | { username?: string; email?: string }, 14 | ApiTypes.CheckAvailabilityResponseDto 15 | >("auth/checkAvailability"); 16 | export const sendEmailVerificationCode = createPostApi< 17 | ApiTypes.SendEmailVerificationCodeRequestDto, 18 | ApiTypes.SendEmailVerificationCodeResponseDto 19 | >("auth/sendEmailVerificationCode", true); 20 | export const register = createPostApi("auth/register", true); 21 | export const resetPassword = createPostApi( 22 | "auth/resetPassword", 23 | true 24 | ); 25 | export const listUserSessions = createPostApi< 26 | ApiTypes.ListUserSessionsRequestDto, 27 | ApiTypes.ListUserSessionsResponseDto 28 | >("auth/listUserSessions", false); 29 | export const revokeUserSession = createPostApi< 30 | ApiTypes.RevokeUserSessionRequestDto, 31 | ApiTypes.RevokeUserSessionResponseDto 32 | >("auth/revokeUserSession", false); 33 | -------------------------------------------------------------------------------- /src/api-generated/modules/group.ts: -------------------------------------------------------------------------------- 1 | // This file is generated automatically, do NOT modify it. 2 | 3 | /// 4 | 5 | import { createGetApi, createPostApi } from "@/api"; 6 | 7 | export const getGroupMeta = createGetApi<{ groupId: string }, ApiTypes.GetGroupMetaResponseDto>("group/getGroupMeta"); 8 | export const searchGroup = createGetApi<{ query: string; wildcard?: string }, ApiTypes.SearchGroupResponseDto>( 9 | "group/searchGroup" 10 | ); 11 | export const createGroup = createPostApi( 12 | "group/createGroup", 13 | false 14 | ); 15 | export const deleteGroup = createPostApi( 16 | "group/deleteGroup", 17 | false 18 | ); 19 | export const renameGroup = createPostApi( 20 | "group/renameGroup", 21 | false 22 | ); 23 | export const addMember = createPostApi( 24 | "group/addMember", 25 | false 26 | ); 27 | export const removeMember = createPostApi< 28 | ApiTypes.RemoveUserFromGroupRequestDto, 29 | ApiTypes.RemoveUserFromGroupResponseDto 30 | >("group/removeMember", false); 31 | export const setGroupAdmin = createPostApi( 32 | "group/setGroupAdmin", 33 | false 34 | ); 35 | export const getGroupList = createGetApi("group/getGroupList"); 36 | export const getGroupMemberList = createPostApi< 37 | ApiTypes.GetGroupMemberListRequestDto, 38 | ApiTypes.GetGroupMemberListResponseDto 39 | >("group/getGroupMemberList", false); 40 | -------------------------------------------------------------------------------- /src/api-generated/modules/homepage.ts: -------------------------------------------------------------------------------- 1 | // This file is generated automatically, do NOT modify it. 2 | 3 | /// 4 | 5 | import { createGetApi, createPostApi } from "@/api"; 6 | 7 | export const getHomepage = createGetApi<{ locale: string }, ApiTypes.GetHomepageResponseDto>("homepage/getHomepage"); 8 | export const getHomepageSettings = createGetApi( 9 | "homepage/getHomepageSettings" 10 | ); 11 | export const updateHomepageSettings = createPostApi< 12 | ApiTypes.UpdateHomepageSettingsRequestDto, 13 | ApiTypes.UpdateHomepageSettingsResponseDto 14 | >("homepage/updateHomepageSettings", false); 15 | -------------------------------------------------------------------------------- /src/api-generated/modules/judge-client.ts: -------------------------------------------------------------------------------- 1 | // This file is generated automatically, do NOT modify it. 2 | 3 | /// 4 | 5 | import { createGetApi, createPostApi } from "@/api"; 6 | 7 | export const addJudgeClient = createPostApi( 8 | "judgeClient/addJudgeClient", 9 | false 10 | ); 11 | export const deleteJudgeClient = createPostApi< 12 | ApiTypes.DeleteJudgeClientRequestDto, 13 | ApiTypes.DeleteJudgeClientResponseDto 14 | >("judgeClient/deleteJudgeClient", false); 15 | export const resetJudgeClientKey = createPostApi< 16 | ApiTypes.ResetJudgeClientKeyRequestDto, 17 | ApiTypes.ResetJudgeClientKeyResponseDto 18 | >("judgeClient/resetJudgeClientKey", false); 19 | export const listJudgeClients = createGetApi( 20 | "judgeClient/listJudgeClients" 21 | ); 22 | -------------------------------------------------------------------------------- /src/api-generated/modules/migration.ts: -------------------------------------------------------------------------------- 1 | // This file is generated automatically, do NOT modify it. 2 | 3 | /// 4 | 5 | import { createGetApi, createPostApi } from "@/api"; 6 | 7 | export const migrateUser = createPostApi( 8 | "migration/migrateUser", 9 | false 10 | ); 11 | export const queryUserMigrationInfo = createPostApi< 12 | ApiTypes.QueryUserMigrationInfoRequestDto, 13 | ApiTypes.QueryUserMigrationInfoResponseDto 14 | >("migration/queryUserMigrationInfo", false); 15 | -------------------------------------------------------------------------------- /src/api-generated/modules/submission.ts: -------------------------------------------------------------------------------- 1 | // This file is generated automatically, do NOT modify it. 2 | 3 | /// 4 | 5 | import { createGetApi, createPostApi } from "@/api"; 6 | 7 | export const submit = createPostApi("submission/submit", true); 8 | export const querySubmission = createPostApi( 9 | "submission/querySubmission", 10 | false 11 | ); 12 | export const getSubmissionDetail = createPostApi< 13 | ApiTypes.GetSubmissionDetailRequestDto, 14 | ApiTypes.GetSubmissionDetailResponseDto 15 | >("submission/getSubmissionDetail", false); 16 | export const downloadSubmissionFile = createPostApi< 17 | ApiTypes.DownloadSubmissionFileRequestDto, 18 | ApiTypes.DownloadSubmissionFileResponseDto 19 | >("submission/downloadSubmissionFile", false); 20 | export const querySubmissionStatistics = createPostApi< 21 | ApiTypes.QuerySubmissionStatisticsRequestDto, 22 | ApiTypes.QuerySubmissionStatisticsResponseDto 23 | >("submission/querySubmissionStatistics", false); 24 | export const rejudgeSubmission = createPostApi< 25 | ApiTypes.RejudgeSubmissionRequestDto, 26 | ApiTypes.RejudgeSubmissionResponseDto 27 | >("submission/rejudgeSubmission", false); 28 | export const cancelSubmission = createPostApi< 29 | ApiTypes.CancelSubmissionRequestDto, 30 | ApiTypes.CancelSubmissionResponseDto 31 | >("submission/cancelSubmission", false); 32 | export const setSubmissionPublic = createPostApi< 33 | ApiTypes.SetSubmissionPublicRequestDto, 34 | ApiTypes.SetSubmissionPublicResponseDto 35 | >("submission/setSubmissionPublic", false); 36 | export const deleteSubmission = createPostApi< 37 | ApiTypes.DeleteSubmissionRequestDto, 38 | ApiTypes.DeleteSubmissionResponseDto 39 | >("submission/deleteSubmission", false); 40 | -------------------------------------------------------------------------------- /src/assets/default-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/prism-tomorrow-night.css: -------------------------------------------------------------------------------- 1 | .highlighted { 2 | color: #ccc; 3 | } 4 | 5 | .token.comment, 6 | .token.block-comment, 7 | .token.prolog, 8 | .token.doctype, 9 | .token.cdata { 10 | color: #999; 11 | } 12 | 13 | .token.punctuation { 14 | color: #ccc; 15 | } 16 | 17 | .token.tag, 18 | .token.attr-name, 19 | .token.namespace, 20 | .token.deleted { 21 | color: #e2777a; 22 | } 23 | 24 | .token.function-name { 25 | color: #6196cc; 26 | } 27 | 28 | .token.boolean, 29 | .token.number, 30 | .token.function { 31 | color: #f08d49; 32 | } 33 | 34 | .token.property, 35 | .token.class-name, 36 | .token.constant, 37 | .token.symbol { 38 | color: #f8c555; 39 | } 40 | 41 | .token.selector, 42 | .token.important, 43 | .token.atrule, 44 | .token.keyword, 45 | .token.builtin { 46 | color: #cc99cd; 47 | } 48 | 49 | .token.string, 50 | .token.char, 51 | .token.attr-value, 52 | .token.regex, 53 | .token.variable { 54 | color: #7ec699; 55 | } 56 | 57 | .token.operator, 58 | .token.entity, 59 | .token.url { 60 | color: #67cdcc; 61 | } 62 | 63 | .token.important, 64 | .token.bold { 65 | font-weight: bold; 66 | } 67 | .token.italic { 68 | font-style: italic; 69 | } 70 | 71 | .token.entity { 72 | cursor: help; 73 | } 74 | 75 | .token.inserted { 76 | color: green; 77 | } 78 | -------------------------------------------------------------------------------- /src/assets/prism-tomorrow.css: -------------------------------------------------------------------------------- 1 | .highlighted { 2 | color: #4d4d4c; 3 | } 4 | 5 | .token.comment, 6 | .token.block-comment, 7 | .token.prolog, 8 | .token.doctype, 9 | .token.cdata { 10 | color: #8e908c; 11 | } 12 | 13 | .token.punctuation { 14 | color: #4d4d4c; 15 | } 16 | 17 | .token.tag, 18 | .token.attr-name, 19 | .token.namespace, 20 | .token.deleted { 21 | color: #8959a8; 22 | } 23 | 24 | .token.function-name { 25 | color: #4271ae; 26 | } 27 | 28 | .token.function { 29 | color: #4271ae; 30 | } 31 | 32 | .token.class-name, 33 | .token.constant, 34 | .token.boolean, 35 | .token.number, 36 | .token.symbol { 37 | color: #f5871f; 38 | } 39 | 40 | .token.property, 41 | .token.selector, 42 | .token.important, 43 | .token.atrule, 44 | .token.keyword, 45 | .token.builtin { 46 | color: #8959a8; 47 | } 48 | 49 | .token.string, 50 | .token.char, 51 | .token.attr-value, 52 | .token.regex, 53 | .token.variable { 54 | color: #718c00; 55 | } 56 | 57 | .token.operator, 58 | .token.entity, 59 | .token.url { 60 | color: #3e999f; 61 | } 62 | 63 | .token.inserted { 64 | color: green; 65 | } 66 | -------------------------------------------------------------------------------- /src/components/CodeBox.module.less: -------------------------------------------------------------------------------- 1 | .codeBox { 2 | margin-top: 0px; 3 | margin-bottom: 14px; 4 | 5 | &:last-child { 6 | margin-bottom: 0; 7 | } 8 | } 9 | 10 | .codeBoxSegment { 11 | overflow: hidden; 12 | padding: 0 !important; 13 | } 14 | 15 | .codeBoxContent { 16 | margin-top: 0; 17 | margin-bottom: 0; 18 | padding: 1em 1em; 19 | height: 100%; 20 | overflow: auto; 21 | overflow: overlay; 22 | overflow-y: hidden; 23 | 24 | &.wrap { 25 | white-space: pre-wrap; 26 | } 27 | } 28 | 29 | .omittedLabel { 30 | position: absolute; 31 | background-color: var(--theme-background-transparent); 32 | color: var(--theme-foreground-transparent); 33 | right: 0; 34 | bottom: 0; 35 | padding: 14px 14px; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/CodeEditor.module.less: -------------------------------------------------------------------------------- 1 | .editorContainer { 2 | border: 1px solid var(--theme-border); 3 | width: 100%; 4 | height: 100%; 5 | position: relative; 6 | 7 | &:focus-within { 8 | border-color: var(--theme-input-border-focus); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/DiscussionSearch.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { observer } from "mobx-react"; 3 | 4 | import api from "@/api"; 5 | import { appState } from "@/appState"; 6 | import { getDiscussionDisplayTitle } from "@/pages/discussion/utils"; 7 | import { useLocalizer } from "@/utils/hooks"; 8 | import toast from "@/utils/toast"; 9 | import { EmojiRenderer } from "./EmojiRenderer"; 10 | import PreviewSearch from "./PreviewSearch"; 11 | 12 | interface DiscussionSearchProps { 13 | className?: string; 14 | queryParameters?: Omit< 15 | ApiTypes.QueryDiscussionsRequestDto, 16 | "locale" | "keyword" | "titleOnly" | "skipCount" | "takeCount" 17 | >; 18 | onResultSelect: (discussion: ApiTypes.QueryDiscussionsResponseDiscussionDto) => void; 19 | onEnterPress?: (searchKeyword: string) => void; 20 | } 21 | 22 | const SEARCH_DISCUSSION_PREVIEW_LIST_LENGTH = appState.serverPreference.pagination.searchDiscussionsPreview; 23 | 24 | export let DiscussionSearch: React.FC = props => { 25 | const _ = useLocalizer("discussions.search_discussion"); 26 | 27 | return ( 28 | result.meta.id} 33 | onSearch={async input => { 34 | if (!input) return []; 35 | 36 | const { requestError, response } = await api.discussion.queryDiscussions( 37 | Object.assign({ locale: appState.locale }, props.queryParameters, { 38 | keyword: input, 39 | titleOnly: true, 40 | skipCount: 0, 41 | takeCount: SEARCH_DISCUSSION_PREVIEW_LIST_LENGTH 42 | }) 43 | ); 44 | 45 | if (requestError) toast.error(requestError(_)); 46 | else return response.discussions; 47 | 48 | return []; 49 | }} 50 | onRenderResult={result => ( 51 | 52 |
{getDiscussionDisplayTitle(result.meta.title, _)}
53 |
54 | )} 55 | onResultSelect={props.onResultSelect} 56 | onEnterPress={props.onEnterPress} 57 | /> 58 | ); 59 | }; 60 | 61 | DiscussionSearch = observer(DiscussionSearch); 62 | -------------------------------------------------------------------------------- /src/components/EmojiRenderer.module.less: -------------------------------------------------------------------------------- 1 | .emoji { 2 | height: 1.1em !important; 3 | width: 1.1em !important; 4 | vertical-align: middle !important; 5 | position: relative; 6 | top: -0.1em; 7 | margin: -0.5em 0 !important; 8 | display: initial !important; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/EmojiRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { Ref } from "semantic-ui-react"; 3 | import twemoji from "twemoji"; 4 | 5 | import style from "./EmojiRenderer.module.less"; 6 | 7 | interface EmojiRendererProps { 8 | children: React.ReactElement; 9 | } 10 | 11 | export const getTwemojiOptions = (inline: boolean) => 12 | ({ 13 | base: `${window.cdnjs}/twemoji/${EXTERNAL_PACKAGE_VERSION["twemoji"]}/`, 14 | size: "svg", 15 | ext: ".svg", 16 | className: inline ? style.emoji : "", 17 | callback: (icon, options: TwemojiOptions, variant) => { 18 | if (icon === "1f1f9-1f1fc") icon = "1f1e8-1f1f3"; 19 | 20 | switch (icon) { 21 | // © copyright 22 | case "a9": 23 | // ® registered trademark 24 | case "ae": 25 | // ™ trademark 26 | case "2122": 27 | return false; 28 | } 29 | 30 | return `${options.base}${options.size}/${icon}${options.ext}`; 31 | } 32 | } as Partial); 33 | 34 | export const EmojiRenderer: React.FC = props => { 35 | const refElement = useRef(); 36 | useEffect(() => { 37 | if (refElement.current) twemoji.parse(refElement.current, getTwemojiOptions(true)); 38 | }); 39 | 40 | return {props.children}; 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/GlobalProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { useNProgress } from "@tanem/react-nprogress"; 2 | import React from "react"; 3 | 4 | interface GlobalProgressBarProps { 5 | isAnimating: boolean; 6 | } 7 | 8 | const GlobalProgressBar: React.FC = ({ isAnimating }) => { 9 | const { animationDuration, isFinished, progress } = useNProgress({ 10 | animationDuration: 250, 11 | incrementDuration: 300, 12 | isAnimating 13 | }); 14 | 15 | return ( 16 |
23 |
36 |
48 |
49 |
50 | ); 51 | }; 52 | 53 | export default GlobalProgressBar; 54 | -------------------------------------------------------------------------------- /src/components/HorizontalScroll.module.less: -------------------------------------------------------------------------------- 1 | .outerWrapper { 2 | position: relative; 3 | overflow: hidden; 4 | } 5 | 6 | .middleWrapper { 7 | position: absolute; 8 | right: 0; 9 | transform: rotate(-90deg) translateY(-100%); 10 | transform-origin: right top; 11 | overflow-x: hidden; 12 | overflow-y: scroll; 13 | } 14 | 15 | .innerWrapper { 16 | transform: rotate(90deg) translateX(100%); 17 | transform-origin: right top; 18 | overflow: visible; 19 | 20 | &::after { 21 | content: ' '; 22 | display: table; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/HorizontalScroll.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | 3 | import style from "./HorizontalScroll.module.less"; 4 | import { useBoundingRect } from "@/utils/hooks"; 5 | 6 | interface HorizontalScrollProps { 7 | className?: string; 8 | } 9 | 10 | const HorizontalScroll: React.FC = props => { 11 | const [width, setWidthReferenceElement] = useBoundingRect("width"); 12 | const [height, setHeightReferenceElement] = useBoundingRect("height"); 13 | 14 | const refScrollBarSizeTester = useRef(); 15 | if (!refScrollBarSizeTester.current) { 16 | const div = document.createElement("div"); 17 | div.style.overflowY = "scroll"; 18 | div.style.width = div.style.height = "100px"; 19 | div.style.position = "fixed"; 20 | div.style.top = div.style.left = "-1000px"; 21 | refScrollBarSizeTester.current = div; 22 | } 23 | 24 | const [scrollBarSize, setScrollBarSize] = useState(0); 25 | useEffect(() => { 26 | function onResize() { 27 | const div = refScrollBarSizeTester.current; 28 | if (div.parentNode) return; 29 | 30 | document.body.appendChild(div); 31 | setScrollBarSize(div.offsetWidth - div.clientWidth); 32 | document.body.removeChild(div); 33 | } 34 | 35 | onResize(); 36 | window.addEventListener("resize", onResize); 37 | return () => window.removeEventListener("resize", onResize); 38 | }); 39 | 40 | return ( 41 |
46 |
50 |
51 | {props.children} 52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default HorizontalScroll; 59 | -------------------------------------------------------------------------------- /src/components/LazyCodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from "react"; 2 | import { Loader } from "semantic-ui-react"; 3 | import { loader as monacoLoader } from "@monaco-editor/react"; 4 | import type { CodeEditorProps } from "./CodeEditor"; 5 | 6 | import style from "./CodeEditor.module.less"; 7 | 8 | const CodeEditor = lazy(async () => { 9 | monacoLoader.config({ 10 | paths: { 11 | vs: `${window.cdnjs}/monaco-editor/${EXTERNAL_PACKAGE_VERSION["monaco-editor"]}/min/vs` 12 | } 13 | }); 14 | window["Monaco"] = await monacoLoader.init(); 15 | return import("./CodeEditor"); 16 | }); 17 | 18 | const LazyCodeEditor: React.FC = props => { 19 | const loading = ( 20 |
21 | 22 |
23 | ); 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default LazyCodeEditor; 32 | -------------------------------------------------------------------------------- /src/components/LazyPermissionManager.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import type { PermissionManagerProps } from "./PermissionManager"; 3 | 4 | export type { PermissionManagerProps } from "./PermissionManager"; 5 | 6 | const LazyPermissionManager: React.FC = props => { 7 | const [PermissionManagerComponent, setPermissionManagerComponent] = useState>(); 8 | 9 | const [loading, setLoading] = useState(false); 10 | const refFirstTimeMounted = useRef(); 11 | const refFirstOpenPromiseResolve = useRef<(success: boolean) => void>(); 12 | 13 | useEffect(() => { 14 | if (!refFirstTimeMounted.current) { 15 | const open = async () => { 16 | if (loading) return; 17 | setLoading(true); 18 | 19 | const promise = new Promise(resolve => (refFirstOpenPromiseResolve.current = resolve)); 20 | 21 | try { 22 | setPermissionManagerComponent((await import("./PermissionManager")).default); 23 | } catch (e) { 24 | console.log(e); 25 | } 26 | 27 | setLoading(false); 28 | 29 | return await promise; 30 | }; 31 | 32 | if (typeof props.refOpen === "function") props.refOpen(open); 33 | else (props.refOpen as React.MutableRefObject<() => Promise>).current = open; 34 | } 35 | }, [props.refOpen]); 36 | 37 | return PermissionManagerComponent ? ( 38 | { 41 | if (typeof props.refOpen === "function") props.refOpen(open); 42 | else (props.refOpen as React.MutableRefObject<() => Promise>).current = open; 43 | 44 | if (!refFirstTimeMounted.current) { 45 | refFirstTimeMounted.current = true; 46 | open().then(refFirstOpenPromiseResolve.current); 47 | } 48 | }} 49 | /> 50 | ) : null; 51 | }; 52 | 53 | export default LazyPermissionManager; 54 | -------------------------------------------------------------------------------- /src/components/LocalizeTab.module.less: -------------------------------------------------------------------------------- 1 | .toolbarMenuIconItem { 2 | width: 43px; 3 | padding: 0 !important; 4 | } 5 | 6 | .localeTabPane { 7 | border: none !important; 8 | padding: 2px !important; 9 | margin-bottom: 0 !important; 10 | 11 | > :last-child { 12 | margin-bottom: 0 !important; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Pagination.module.less: -------------------------------------------------------------------------------- 1 | .pagination { 2 | :global(.item) { 3 | outline: none; 4 | } 5 | } 6 | 7 | .pagination.simple { 8 | :global(.item) { 9 | outline: none; 10 | 11 | &[type=pageItem] { 12 | display: none !important; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/PermissionManager.module.less: -------------------------------------------------------------------------------- 1 | .dialogHeader { 2 | position: relative; 3 | } 4 | 5 | .dialogHeaderInfo { 6 | position: absolute; 7 | right: 1.5rem; 8 | top: 50%; 9 | transform: translateY(-50%); 10 | font-weight: normal; 11 | opacity: 0.5; 12 | } 13 | 14 | .dialogMessage { 15 | padding: 1em 0.85em !important; 16 | white-space: pre-wrap; 17 | } 18 | 19 | .columnUsername { 20 | :global(.ui.image.header) { 21 | display: flex !important; 22 | align-items: center; 23 | 24 | img { 25 | flex-shrink: 0; 26 | } 27 | 28 | :global(.content) { 29 | overflow: hidden; 30 | text-overflow: ellipsis; 31 | } 32 | } 33 | 34 | // mobile 35 | .emailInMobileView { 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | } 39 | } 40 | 41 | .columnGroupName { 42 | white-space: nowrap; 43 | } 44 | 45 | .columnDropdown { 46 | overflow: visible !important; 47 | white-space: nowrap; 48 | } 49 | 50 | .avatar { 51 | width: 27.5px !important; 52 | height: 27.5px !important; 53 | } 54 | 55 | // mobile 56 | .userResultContainer { 57 | width: 100%; 58 | } 59 | 60 | .userResult { 61 | // mobile 62 | &.firstRow { 63 | display: flex; 64 | align-items: center; 65 | } 66 | 67 | // mobile 68 | &.email { 69 | overflow: hidden; 70 | text-overflow: ellipsis; 71 | } 72 | 73 | &.avatar { 74 | margin-right: 10px; 75 | flex-shrink: 0; 76 | } 77 | 78 | &.username { 79 | font-weight: bolder; 80 | 81 | @media only screen and (min-width: 768px) { 82 | width: 35%; 83 | } 84 | 85 | // mobile 86 | @media only screen and (max-width: 767px) { 87 | overflow: hidden; 88 | text-overflow: ellipsis; 89 | } 90 | } 91 | } 92 | 93 | .groupIcon { 94 | margin-right: 9px !important; 95 | } 96 | 97 | .noGroupGranted { 98 | opacity: 0.5; 99 | height: calc(33.5px * 1.5); 100 | font-weight: normal; 101 | } 102 | 103 | .groupName { 104 | overflow: hidden; 105 | text-overflow: ellipsis; 106 | height: 18.5px; 107 | margin-top: -2px; 108 | margin-bottom: -2px; 109 | } 110 | -------------------------------------------------------------------------------- /src/components/PreviewSearch.module.less: -------------------------------------------------------------------------------- 1 | .search :global(.results) { 2 | width: 100% !important; 3 | } 4 | 5 | @media only screen and (min-width: 768px) { 6 | .search { 7 | width: 280px; 8 | display: inline-block; 9 | 10 | :global(.ui.input) { 11 | width: 100%; 12 | } 13 | } 14 | } 15 | 16 | @media only screen and (max-width: 767px) { 17 | .search { 18 | flex-grow: 1; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/PseudoLink.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * A fake element, looks like but acts like a button 3 | */ 4 | 5 | import React from "react"; 6 | 7 | type PseudoLinkProps = React.AnchorHTMLAttributes; 8 | 9 | const PseudoLink: React.FC = React.memo(props => { 10 | return ( 11 | element && element.setAttribute("href", "javascript:void(0)")}> 12 | {props.children} 13 | 14 | ); 15 | }); 16 | 17 | export default PseudoLink; 18 | -------------------------------------------------------------------------------- /src/components/ScoreText.module.less: -------------------------------------------------------------------------------- 1 | .score_0 { 2 | color: var(--theme-score-0); 3 | } 4 | 5 | .score_1 { 6 | color: var(--theme-score-1); 7 | } 8 | 9 | .score_2 { 10 | color: var(--theme-score-2); 11 | } 12 | 13 | .score_3 { 14 | color: var(--theme-score-3); 15 | } 16 | 17 | .score_4 { 18 | color: var(--theme-score-4); 19 | } 20 | 21 | .score_5 { 22 | color: var(--theme-score-5); 23 | } 24 | 25 | .score_6 { 26 | color: var(--theme-score-6); 27 | } 28 | 29 | .score_7 { 30 | color: var(--theme-score-7); 31 | } 32 | 33 | .score_8 { 34 | color: var(--theme-score-8); 35 | } 36 | 37 | .score_9 { 38 | color: var(--theme-score-9); 39 | } 40 | 41 | .score_10 { 42 | color: var(--theme-score-10); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ScoreText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import style from "./ScoreText.module.less"; 4 | 5 | interface ScoreTextProps { 6 | score: number; 7 | } 8 | 9 | const ScoreText: React.FC = props => { 10 | return {props.score}; 11 | }; 12 | 13 | export default ScoreText; 14 | 15 | export function getScoreColor(score: number | string): string { 16 | return [ 17 | "#ff4f4f", 18 | "#ff694f", 19 | "#f8603a", 20 | "#fc8354", 21 | "#fa9231", 22 | "#f7bb3b", 23 | "#ecdb44", 24 | "#e2ec52", 25 | "#b0d628", 26 | "#93b127", 27 | "#25ad40" 28 | ][Math.floor((Number(score) || 0) / 10)]; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/StatusText.module.less: -------------------------------------------------------------------------------- 1 | .icon { 2 | margin-right: 6px !important; 3 | 4 | &.noMarginRight { 5 | margin-right: 0 !important; 6 | } 7 | } 8 | 9 | .Pending { 10 | color: var(--theme-status-pending); 11 | } 12 | 13 | .ConfigurationError { 14 | color: var(--theme-status-configuration-error); 15 | } 16 | 17 | .SystemError { 18 | color: var(--theme-status-system-error); 19 | } 20 | 21 | .CompilationError { 22 | color: var(--theme-status-compilation-error); 23 | } 24 | 25 | .Canceled { 26 | color: var(--theme-status-canceled); 27 | } 28 | 29 | .FileError { 30 | color: var(--theme-status-file-error); 31 | } 32 | 33 | .RuntimeError { 34 | color: var(--theme-status-runtime-error); 35 | } 36 | 37 | .TimeLimitExceeded { 38 | color: var(--theme-status-time-limit-exceeded); 39 | } 40 | 41 | .MemoryLimitExceeded { 42 | color: var(--theme-status-memory-limit-exceeded); 43 | } 44 | 45 | .OutputLimitExceeded { 46 | color: var(--theme-status-output-limit-exceeded); 47 | } 48 | 49 | .PartiallyCorrect { 50 | color: var(--theme-status-partially-correct); 51 | } 52 | 53 | .WrongAnswer { 54 | color: var(--theme-status-wrong-answer); 55 | } 56 | 57 | .Accepted { 58 | color: var(--theme-status-accepted); 59 | } 60 | 61 | .JudgementFailed { 62 | color: var(--theme-status-judgement-failed); 63 | } 64 | 65 | .Waiting { 66 | color: var(--theme-status-waiting); 67 | } 68 | 69 | .Preparing { 70 | color: var(--theme-status-preparing); 71 | } 72 | 73 | .Compiling { 74 | color: var(--theme-status-compiling); 75 | } 76 | 77 | .Running { 78 | color: var(--theme-status-running); 79 | } 80 | 81 | .Skipped { 82 | color: var(--theme-status-skipped); 83 | } 84 | -------------------------------------------------------------------------------- /src/components/StatusText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Icon, SemanticICONS } from "semantic-ui-react"; 3 | 4 | import style from "./StatusText.module.less"; 5 | 6 | import { SubmissionStatusAll } from "@/interfaces/SubmissionStatus"; 7 | 8 | const icons: Record = { 9 | Pending: "hourglass half", 10 | ConfigurationError: "code", 11 | SystemError: "server", 12 | CompilationError: "code", 13 | Canceled: "remove", 14 | FileError: "file outline", 15 | RuntimeError: "bomb", 16 | TimeLimitExceeded: "clock", 17 | MemoryLimitExceeded: "microchip", 18 | OutputLimitExceeded: "print", 19 | PartiallyCorrect: "minus", 20 | WrongAnswer: "remove", 21 | Accepted: "checkmark", 22 | JudgementFailed: "server", 23 | Waiting: "hourglass half", 24 | Preparing: "spinner", 25 | Compiling: "spinner", 26 | Running: "spinner", 27 | Skipped: "fast forward" 28 | }; 29 | 30 | interface StatusIconProps { 31 | status: string; 32 | noMarginRight?: boolean; 33 | } 34 | 35 | export const StatusIcon: React.FC = props => ( 36 | 37 | 42 | 43 | ); 44 | 45 | interface StatusTextProps { 46 | // This is the text to display 47 | statusText?: string; 48 | // This is the status for icon and color, an enum value, without spaces between each work 49 | // If statusText is unset, this will be transformed to the text to display 50 | status: string; 51 | } 52 | 53 | const StatusText: React.FC = props => { 54 | const text = props.statusText || props.status.replace(/([A-Z])/g, " $1").trimStart(); 55 | return ( 56 | 57 | 62 | {text} 63 | 64 | ); 65 | }; 66 | 67 | export default StatusText; 68 | -------------------------------------------------------------------------------- /src/components/TableCellSearchDropdown.module.less: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | > input { 3 | line-height: 19.5px; 4 | } 5 | 6 | > :global(.menu) { 7 | top: 30.5px !important; 8 | left: -12px !important; 9 | width: calc(100% + 24px) !important; 10 | min-width: 0 !important; 11 | 12 | :global(.ui.compact.table) & { 13 | left: -11px !important; 14 | width: calc(100% + 22px) !important; 15 | } 16 | 17 | &:empty { 18 | display: none !important; 19 | } 20 | } 21 | 22 | > :global(i.dropdown.icon) { 23 | width: 18px !important; 24 | position: absolute !important; 25 | right: 9.5px; 26 | top: 0.75px; 27 | display: none !important; 28 | 29 | &::before, &::after { 30 | top: unset !important; 31 | left: unset !important; 32 | margin: 0 !important; 33 | width: 18px !important; 34 | height: 18px !important; 35 | } 36 | } 37 | 38 | &:global(.loading) > :global(i.dropdown.icon) { 39 | display: block !important; 40 | } 41 | } 42 | 43 | .resultItem { 44 | display: flex; 45 | align-items: center; 46 | width: calc(100% + 10px); 47 | margin-left: -5px; 48 | 49 | :global(.default.text) > & { 50 | display: none; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/TimeAgo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { observer } from "mobx-react"; 3 | import * as timeago from "timeago.js"; 4 | 5 | import formatDateTime from "@/utils/formatDateTime"; 6 | import { appState } from "@/appState"; 7 | 8 | interface TimeAgoProps { 9 | time: Date; 10 | dateOnly?: boolean; 11 | } 12 | 13 | function fixLater(time: Date, relative: Date) { 14 | // Accept the client's time is <= 1 minute slower than server's time 15 | if (time >= relative) return new Date(+time - Math.min(+time - +relative + 1, 60 * 1000)); 16 | return time; 17 | } 18 | 19 | let TimeAgo: React.FC = props => { 20 | // Update per 30s 21 | const UPDATE_INTERVAL = 30 * 1000; 22 | 23 | // Use time ago if within 30 days 24 | const MAX_TIME_AGO = 30 * 24 * 60 * 60 * 1000; 25 | 26 | const [relativeDate, setRelativeDate] = useState(new Date()); 27 | 28 | const getUseTimeAgo = () => +relativeDate - +props.time <= MAX_TIME_AGO; 29 | const [useTimeAgo, setUseTimeAgo] = useState(getUseTimeAgo()); 30 | 31 | useEffect(() => { 32 | if (useTimeAgo) { 33 | const id = setInterval(() => setRelativeDate(new Date()), UPDATE_INTERVAL); 34 | return () => clearInterval(id); 35 | } 36 | }, [useTimeAgo]); 37 | 38 | useEffect(() => { 39 | if (getUseTimeAgo() !== useTimeAgo) setUseTimeAgo(!useTimeAgo); 40 | }, [relativeDate, props.time]); 41 | 42 | const fullDateTime = formatDateTime(props.time, props.dateOnly)[1]; 43 | 44 | return ( 45 | <> 46 | {useTimeAgo ? ( 47 | 48 | {timeago.format(fixLater(props.time, relativeDate), appState.locale, { 49 | relativeDate: relativeDate 50 | })} 51 | 52 | ) : ( 53 | fullDateTime 54 | )} 55 | 56 | ); 57 | }; 58 | 59 | TimeAgo = observer(TimeAgo); 60 | 61 | export default TimeAgo; 62 | -------------------------------------------------------------------------------- /src/components/UserLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { UserMeta } from "@/interfaces/UserMeta"; 4 | import { Link } from "@/utils/hooks"; 5 | 6 | interface UserLinkProps { 7 | user: UserMeta; 8 | } 9 | 10 | const UserLink: React.FC = props => { 11 | // TODO: rating color 12 | const escapedUsername = encodeURIComponent(props.user.username); 13 | return {props.children || props.user.username}; 14 | }; 15 | 16 | export default UserLink; 17 | -------------------------------------------------------------------------------- /src/components/UserSearch.module.less: -------------------------------------------------------------------------------- 1 | .result { 2 | display: flex; 3 | align-items: center; 4 | 5 | .avatar { 6 | width: 27.5px !important; 7 | height: 27.5px !important; 8 | flex-shrink: 0; 9 | } 10 | 11 | .username { 12 | font-weight: bolder; 13 | margin-left: 10px; 14 | flex-shrink: 1; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/UserSearch.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import style from "./UserSearch.module.less"; 4 | 5 | import api from "@/api"; 6 | import { useLocalizer } from "@/utils/hooks"; 7 | import toast from "@/utils/toast"; 8 | import UserAvatar from "./UserAvatar"; 9 | import PreviewSearch from "./PreviewSearch"; 10 | 11 | interface UserSearchProps { 12 | className?: string; 13 | placeholder?: string; 14 | onResultSelect: (user: ApiTypes.UserMetaDto) => void; 15 | } 16 | 17 | let UserSearch: React.FC = props => { 18 | const _ = useLocalizer("components.user_search"); 19 | 20 | return ( 21 | result.id} 26 | onSearch={async input => { 27 | const wildcardStart = input.startsWith("*"); 28 | if (wildcardStart) input = input.substr(1); 29 | if (!input) return []; 30 | 31 | const { requestError, response } = await api.user.searchUser({ 32 | query: input, 33 | wildcard: wildcardStart ? "Both" : "End" 34 | }); 35 | 36 | if (requestError) toast.error(requestError(_)); 37 | else return response.userMetas; 38 | 39 | return []; 40 | }} 41 | onRenderResult={result => ( 42 |
43 | 44 |
{result.username}
45 |
46 | )} 47 | onResultSelect={props.onResultSelect} 48 | /> 49 | ); 50 | }; 51 | 52 | export default UserSearch; 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The initialization sequence: 3 | * 4 | * index.ts (vite entry, await polyfill & xdomain) 5 | * => index.tsx (app entry) 6 | * => misc/webfonts.ts 7 | * => initApp.ts (session initialization) 8 | * => appState.ts (app global state) 9 | * => misc/analytics.js 10 | * => App.tsx (top-level react component) 11 | * => AppRouter.tsx (react routing) 12 | * => Layout.tsx (app view layout) 13 | * => page routes 14 | */ 15 | import "./index.tsx"; 16 | 17 | export {}; // Fix the error "All files must be modules when the '--isolatedModules' flag is provided". 18 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./themes"; 3 | import "./index.less"; 4 | import "./misc/fonts"; 5 | import App from "./App"; 6 | 7 | import initApp from "./initApp"; 8 | 9 | class ErrorBoundary extends React.Component<{}, { hasError: boolean }> { 10 | constructor(props: {}) { 11 | super(props); 12 | this.state = { hasError: false }; 13 | } 14 | 15 | static getDerivedStateFromError() { 16 | return { hasError: true }; 17 | } 18 | 19 | componentDidCatch(error: any, errorInfo: any) { 20 | window.fatalError( 21 | ["There's a fatal error in the application. It may be a bug.", "应用程序遇到致命错误,这可能是一个 Bug。"], 22 | error.stack 23 | ); 24 | } 25 | 26 | render() { 27 | if (this.state.hasError) return <>; 28 | return this.props.children; 29 | } 30 | } 31 | 32 | initApp() 33 | .then(() => { 34 | const { createRoot } = window["ReactDOM"] as unknown as typeof import("react-dom/client"); 35 | createRoot(document.getElementById("root")).render( 36 | 37 | 38 | 39 | ); 40 | }) 41 | .catch(err => { 42 | window.fatalError( 43 | [ 44 | "There's an error initializing the application. It may be a bug or network issue.", 45 | "初始化应用程序时出错,这可能是一个 Bug 或网络故障。" 46 | ], 47 | err.stack 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /src/interfaces/GroupMeta.ts: -------------------------------------------------------------------------------- 1 | export type GroupMeta = ApiTypes.GroupMetaDto; 2 | -------------------------------------------------------------------------------- /src/interfaces/Locale.ts: -------------------------------------------------------------------------------- 1 | export enum Locale { 2 | zh_CN = "zh_CN", 3 | en_US = "en_US", 4 | ja_JP = "ja_JP" 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/ProblemType.ts: -------------------------------------------------------------------------------- 1 | export enum ProblemType { 2 | Traditional = "Traditional", 3 | Interaction = "Interaction", 4 | SubmitAnswer = "SubmitAnswer" 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/SubmissionStatus.ts: -------------------------------------------------------------------------------- 1 | // Reordered for displaying in a select list 2 | export enum SubmissionStatus { 3 | Accepted = "Accepted", 4 | PartiallyCorrect = "PartiallyCorrect", 5 | WrongAnswer = "WrongAnswer", 6 | RuntimeError = "RuntimeError", 7 | TimeLimitExceeded = "TimeLimitExceeded", 8 | MemoryLimitExceeded = "MemoryLimitExceeded", 9 | CompilationError = "CompilationError", 10 | FileError = "FileError", 11 | OutputLimitExceeded = "OutputLimitExceeded", 12 | JudgementFailed = "JudgementFailed", 13 | ConfigurationError = "ConfigurationError", 14 | SystemError = "SystemError", 15 | Canceled = "Canceled", 16 | Pending = "Pending" 17 | } 18 | 19 | export enum SubmissionStatusAll { 20 | Pending = "Pending", 21 | 22 | ConfigurationError = "ConfigurationError", 23 | SystemError = "SystemError", 24 | Canceled = "Canceled", 25 | 26 | CompilationError = "CompilationError", 27 | 28 | FileError = "FileError", 29 | RuntimeError = "RuntimeError", 30 | TimeLimitExceeded = "TimeLimitExceeded", 31 | MemoryLimitExceeded = "MemoryLimitExceeded", 32 | OutputLimitExceeded = "OutputLimitExceeded", 33 | 34 | PartiallyCorrect = "PartiallyCorrect", 35 | WrongAnswer = "WrongAnswer", 36 | Accepted = "Accepted", 37 | 38 | JudgementFailed = "JudgementFailed", 39 | 40 | Waiting = "Waiting", 41 | Preparing = "Preparing", 42 | Compiling = "Compiling", 43 | Running = "Running", 44 | Skipped = "Skipped" 45 | } 46 | -------------------------------------------------------------------------------- /src/interfaces/UserMeta.ts: -------------------------------------------------------------------------------- 1 | export type UserMeta = ApiTypes.UserMetaDto; 2 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from "@/interfaces/Locale"; 2 | 3 | // See escapeLocalizedMessage in messages/importMessages.js 4 | export function unescapeLocalizedMessage(text: string) { 5 | if (text.startsWith(" ")) text = text.substr(1); 6 | text = text.split("<").join("<").split("&").join("&"); 7 | return text; 8 | } 9 | 10 | const importers = import.meta.glob("./messages/*-*.ts"); 11 | export async function loadLocaleData(locale: Locale): Promise>; 12 | export async function loadLocaleData(locale: Locale): Promise { 13 | return (await importers[`./messages/${locale.replace("_", "-")}.ts`]()).default; 14 | } 15 | 16 | export type LocalizerParameters = Record | React.ReactText[]; 17 | export type Localizer = (messageId: string, parameters?: LocalizerParameters) => string; 18 | export interface ToBeLocalizedText { 19 | (_: Localizer): string; 20 | isToBeLocalizedText: true; 21 | } 22 | 23 | export const makeToBeLocalizedText = (messageId: string, parameters?: LocalizerParameters): ToBeLocalizedText => 24 | Object.assign((_: Localizer) => _(messageId, parameters), { isToBeLocalizedText: true as true }); 25 | 26 | export const isToBeLocalizedText = (object: any): object is ToBeLocalizedText => 27 | "isToBeLocalizedText" in object && object.isToBeLocalizedText === true; 28 | -------------------------------------------------------------------------------- /src/locales/messages/en-US.ts: -------------------------------------------------------------------------------- 1 | import.meta.compileTime("./en-US/import.ts"); 2 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/common.js: -------------------------------------------------------------------------------- 1 | return { 2 | navbar: { 3 | home: "Home", 4 | problem_set: "Problem Set", 5 | contests: "Contests", 6 | submissions: "Submissions", 7 | members: "Members", 8 | discussion: "Discussion" 9 | }, 10 | header: { 11 | user: { 12 | login: "Login", 13 | register: "Register", 14 | profile: "My profile", 15 | submissions: "My submissions", 16 | problems: "My problems", 17 | discussions: "My discussions", 18 | groups: "My groups", 19 | edit_profile: "Edit profile", 20 | preference: "Preference", 21 | logout: "Logout" 22 | } 23 | }, 24 | toast: { 25 | success: "Success", 26 | info: "Info", 27 | warning: "Warning", 28 | error: "Error" 29 | }, 30 | localized_content_unavailable: 31 | "This content is not available in your preferred language, here is the {display_locale} version.", 32 | footer: { 33 | judge_machine: "Judge Machine", 34 | locale: "Language", 35 | github: "Open source" 36 | }, 37 | invalid_url: "Invalid URL.", 38 | request_error: { 39 | 400: "Invalid request.", 40 | 401: "Verification failed. Please refresh and try again.", 41 | 429: "Too many requests. Please try again later.", 42 | 500: "Server error. Please try again later.", 43 | 502: "Couldn't connect to the server. Please try again later.", 44 | 503: "Couldn't connect to the server. Please try again later.", 45 | 504: "Timeout connecting to the server. Please try again later.", 46 | unknown: "Request error: {text}" 47 | }, 48 | confirm_navigation: "Be sure to leave this page? Your changes will not be saved.", 49 | recaptcha: { 50 | copyright: 51 | 'This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.' 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/components/code_box.js: -------------------------------------------------------------------------------- 1 | return { 2 | omitted: "{count} byte omitted", 3 | omitted_s: "{count} bytes omitted" 4 | }; 5 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/components/permission_manager.js: -------------------------------------------------------------------------------- 1 | return { 2 | header: "Permission Manage", 3 | owner: "Owner", 4 | permission_for_users: "Users", 5 | search_users: "Search username to add users ...", 6 | search_users_no_result: "No matching users.", 7 | permission_for_groups: "Groups", 8 | no_group_granted: "No groups granted yet.", 9 | search_groups: "Search group name to add groups ...", 10 | search_groups_no_result: "No matching groups.", 11 | submit: "Submit", 12 | no_submit_permission: "No Permission", 13 | cancel: "Cancel", 14 | confirm_cancel: "Confirm canel", 15 | submit_error: { 16 | PERMISSION_DENIED: "Permission denied.", 17 | NO_SUCH_USER: "No such user with ID {id}.", 18 | NO_SUCH_GROUP: "No such group with ID {id}." 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/components/problem_search.js: -------------------------------------------------------------------------------- 1 | return { 2 | placeholder: "Title / ID ...", 3 | no_result: "No matching problems." 4 | }; 5 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/components/user_search.js: -------------------------------------------------------------------------------- 1 | return { 2 | placeholder: "Search user ...", 3 | no_result: "No matching users." 4 | }; 5 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/discussion_edit.js: -------------------------------------------------------------------------------- 1 | return { 2 | title_new: "Add discussion", 3 | title_update: "Edit discussion", 4 | errors: { 5 | PERMISSION_DENIED: "Permission denied.", 6 | NO_SUCH_PROBLEM: "No such problem.", 7 | NO_SUCH_DISCUSSION: "No such discussion." 8 | }, 9 | header: { 10 | add: "Add discussion", 11 | update: "Edit discussion" 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/discussions.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Discussions", 3 | error: { 4 | PERMISSION_DENIED: "Permission denied.", 5 | NO_SUCH_PROBLEM: "No such problem.", 6 | NO_SUCH_USER: "No such user.", 7 | TAKE_TOO_MANY: "Requested number of items per page exceeds the limit." 8 | }, 9 | breadcrumb: { 10 | discussion: "Discussion", 11 | general: "General", 12 | problem: "Problem" 13 | }, 14 | add_discussion: "Add discussion", 15 | search_discussion: { 16 | placeholder: "Title ...", 17 | no_result: "No matching discussions." 18 | }, 19 | search_icon: { 20 | user: "Publisher", 21 | nonpublic: "Nonpublic" 22 | }, 23 | search_filters: "Search filters", 24 | no_discussions: { 25 | message_search: "No matching discussions", 26 | message_no_search: "No discussions", 27 | back: "Back", 28 | clear_filters: "Clear Query", 29 | create: "Add discussion" 30 | }, 31 | column_title: "Title", 32 | column_problem: "Problem", 33 | column_publisher: "Publisher", 34 | column_reply_count: "Replies", 35 | column_sort_time: "Last updated", 36 | non_public: "Nonpublic", 37 | no_title: "(No title)" 38 | }; 39 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/error.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Error", 3 | error: "Error", 4 | unexpected_error: "Unexpected Error", 5 | back: "Back", 6 | refresh: "Refresh" 7 | }; 8 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/forgot.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Reset password", 3 | reset_your_password: "Reset your password", 4 | email: "Email", 5 | send_email_verification_code: "Send", 6 | email_verification_code_sent: "Email verification code sent.", 7 | email_verification_code: "E-mail Verify Code", 8 | password: "Password", 9 | retype_password: "Retype Password", 10 | submit: "Submit", 11 | success: "Password reset", 12 | errors: { 13 | ALREADY_LOGGEDIN: "You have already logged in.", 14 | NO_SUCH_USER: "No such user.", 15 | INVALID_EMAIL_VERIFICATION_CODE: "Invalid email verification code.", 16 | FAILED_TO_SEND: "Failed to send mail: {errorMessage}", 17 | RATE_LIMITED: "Your operations are too frequent. Please try again later." 18 | }, 19 | empty_email: "Please enter your email address", 20 | invalid_email: "Invalid email address", 21 | invalid_email_verification_code: "Invalid email verification code.", 22 | empty_password: "Please enter your password", 23 | invalid_password: "Password must be in the length of 6 to 32", 24 | passwords_do_not_match: "Passwords don't match", 25 | email_invalid_message: "Invalid email.", 26 | invalid_password_message: "Invalid password.", 27 | passwords_do_not_match_message: "Two passwords don't match." 28 | }; 29 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/groups.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Groups", 3 | header: "Groups", 4 | not_logged_in: "Not logged in.", 5 | no_groups: "No Groups", 6 | group_admin: "Group admin", 7 | errors: { 8 | PERMISSION_DENIED: "Permission denied.", 9 | NO_SUCH_GROUP: "No such group.", 10 | NO_SUCH_USER: "No such user", 11 | USER_NOT_IN_GROUP: "This user is not in the group.", 12 | USER_ALREADY_IN_GROUP: "This user is already in the group.", 13 | DUPLICATE_GROUP_NAME: "The group name has been taken.", 14 | GROUP_ADMIN_CAN_NOT_BE_REMOVED: "Group admin can not be removed." 15 | }, 16 | create_group: "Create group", 17 | create_group_name: "Group name", 18 | confirm_create_group: "Create", 19 | search_to_add_user: "Add user ...", 20 | rename_group: "Rename", 21 | rename_group_new_name: "New name", 22 | confirm_rename_group: "Rename", 23 | delete_group: "Delete", 24 | confirm_delete_group: "Confirm delete group", 25 | remove_member: "Remove", 26 | confirm_remove_member: "Confirm remove member", 27 | invalid_group_name: "Invalid group name." 28 | }; 29 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/home.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Home", 3 | annnouncements: { 4 | header: "Annnouncement", 5 | title: "Title", 6 | date: "Date", 7 | no_annnouncements: "No Annnouncements" 8 | }, 9 | latest_problems: { 10 | header: "Latest Problems", 11 | status: "Status", 12 | problem: "Problem", 13 | updated_time: "Updated Time", 14 | no_problems: "No Problems" 15 | }, 16 | hitokoto: { 17 | header: "Hitokoto (ヒトコト)", 18 | error: "Error loading Hitokoto.", 19 | refresh: "Refresh" 20 | }, 21 | countdown: { 22 | header: "Countdown", 23 | display_time_first: "1", 24 | before_time: "", 25 | after_time_before_event: " before ", 26 | after_days_before_event: " days before ", 27 | after_event: "", 28 | completed_before_event: "", 29 | completed_after_event: " has started" 30 | }, 31 | search_problem: "Search Problem", 32 | top_users: { 33 | header: "Top Users", 34 | username: "Username", 35 | bio: "Bio", 36 | rating: "Rating", 37 | accepted_problem_count: "AC. Count", 38 | no_users: "No Users" 39 | }, 40 | friend_links: "Friend Links" 41 | }; 42 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/home_settings.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Homepage Settings", 3 | errors: { 4 | PERMISSION_DENIED: "Permission denied.", 5 | NO_SUCH_DISCUSSION: "No such discussion with ID {id}." 6 | }, 7 | header: "Homepage Settings", 8 | set_default_locale: "Set default language", 9 | delete_locale: "Delete language", 10 | confirm_delete_locale: "Confirm delete language", 11 | notice: { 12 | header: "Notice", 13 | placeholder: "Notice text ..." 14 | }, 15 | annnouncements: { 16 | header: "Annnouncements", 17 | title: "Title", 18 | date: "Date", 19 | operations: "Operations", 20 | add: "Add annnouncement" 21 | }, 22 | hitokoto: "Hitokoto", 23 | countdown: "Countdown", 24 | friend_links: "Friend Links", 25 | submit: "Submit", 26 | success: "Successfully updated." 27 | }; 28 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/import.ts: -------------------------------------------------------------------------------- 1 | import importMessages from "../importMessages"; 2 | export default () => importMessages(import.meta.url); 3 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/judge_machine.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Judge Machine", 3 | header: "Judge Machine", 4 | refresh: "Refresh", 5 | add: "Add", 6 | add_new_name: "Judge Machine Name", 7 | confirm_add: "Add", 8 | add_success: "Successfully added.", 9 | status: "Status", 10 | name: "Name", 11 | cpu: "CPU", 12 | memory: "Memory", 13 | kernel: "Kernel", 14 | operations: "Operations", 15 | online: "Online", 16 | offline: "Offline", 17 | key: "Key", 18 | reset_key: "Reset key", 19 | confirm_reset_key: "Confirm reset key", 20 | reset_key_success: "Successfully reset key.", 21 | delete: "Delete", 22 | confirm_delete: "Confirm delete", 23 | delete_success: "Successfully deleted.", 24 | error: { 25 | PERMISSION_DENIED: "Permission denied.", 26 | NO_SUCH_JUDGE_CLIENT: "No such judge machine." 27 | }, 28 | no_judge_machine: "No judge machine" 29 | }; 30 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/language.js: -------------------------------------------------------------------------------- 1 | return { 2 | zh_CN: "Chinese (Simplified)", 3 | en_US: "English", 4 | ja_JP: "Japanese" 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/login.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Login", 3 | login_to_your_account: "Login to your account", 4 | username_or_email: "Username / E-mail", 5 | password: "Password", 6 | forgot_password: "Forgot password", 7 | remember: "Remember me", 8 | forget: "Forget password", 9 | new_user: "New user? ", 10 | register: "Register", 11 | login: "Login", 12 | empty_username_or_email: "Please enter your username or email", 13 | invalid_username_or_email: "Invalid username or email", 14 | empty_password: "Please enter your password", 15 | no_such_user: "No such user", 16 | wrong_password: "Wrong password", 17 | welcome: "Welcome back, {username}!", 18 | errors: { 19 | NO_SUCH_USER: "No such user.", 20 | ALREADY_LOGGEDIN: "You have already logged in.", 21 | ALREADY_MIGRATED: "System error. Please try again later.", 22 | DUPLICATE_USERNAME: "Username already taken." 23 | }, 24 | migration: { 25 | title: "Choose a new username", 26 | message: "We are sorry but your username is invalid in our new system. You must choose a new username to continue.", 27 | message_username: 28 | 'Username should contains only A-Z, a-z, 0-9 and "-_.#$" and be in the length of 3 to 24.', 29 | invalid_username: "Invalid username.", 30 | placeholder: "New username", 31 | confirm: "Rename", 32 | cancel: "Cancel" 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/problem_edit.js: -------------------------------------------------------------------------------- 1 | return { 2 | title_edit: "Edit Problem", 3 | title_new: "New Problem", 4 | header_edit: "Edit Problem {idString}", 5 | header_new: "New Problem", 6 | back_to_problem: "Back", 7 | confirm_back_to_problem: "Discard Changes and Back", 8 | submit: "Submit", 9 | no_submit_permission: "No Permission", 10 | submit_success: "Successfully submitted.", 11 | something_empty: "Please fill all titles and section contents.", 12 | error: { 13 | FAILED: "Unknown error.", 14 | NO_SUCH_PROBLEM: "No such problem.", 15 | PERMISSION_DENIED: "Permission denied." 16 | }, 17 | header_samples: "Samples", 18 | header_tags: "Tags", 19 | tags_placeholder: "Search tags to add ...", 20 | no_addable_tags: "No tags to add.", 21 | content_editor: { 22 | title: "Title", 23 | preview_all: "Preview", 24 | default: "Default language", 25 | add_default_sections: "Add default sections", 26 | confirm_delete: "Confirm delete language", 27 | section_title: "Section Title", 28 | preview: "Preview", 29 | add_section: { 30 | before_this_section: "Before this section", 31 | after_this_section: "After this section" 32 | }, 33 | section_content: "Section Content", 34 | new_sample: "New Sample", 35 | sample_input: "Input", 36 | sample_output: "Output", 37 | sample_explanation: "Explanation", 38 | section_options: { 39 | move_up: "Move Up", 40 | move_down: "Move Down", 41 | delete: "Delete", 42 | confirm_delete: "Confirm Delete" 43 | }, 44 | section_type: { 45 | text: "Text", 46 | sample: "Sample" 47 | } 48 | }, 49 | sample_editor: { 50 | add_sample_when_empty: "Add Sample", 51 | sample_id: "Sample ID", 52 | warning: { 53 | not_referenced: 'Not referenced in language "{language}"', 54 | multiple_references: 'Referenced {referenceCount} times in language "{language}"' 55 | }, 56 | add_sample: { 57 | before_this_sample: "Before this sample", 58 | after_this_sample: "After this sample" 59 | }, 60 | options: { 61 | move_up: "Move Up", 62 | move_down: "Move Down", 63 | delete: "Delete", 64 | confirm_delete: "Confirm Delete" 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/problem_files.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Problem Files", 3 | header_testdata: "Test Data", 4 | header_additional_files: "Additional Files", 5 | filename: "Filename", 6 | size: "Size", 7 | operations_and_status: "Op. / Status", 8 | operations: "Op.", 9 | delete: "Delete", 10 | download_as_archive: "Download as Archive", 11 | download_as_archive_error: "Error downloading file {filename}: {error}", 12 | confirm_delete: "Confirm delete", 13 | new_filename: "New filename", 14 | rename: "Rename", 15 | no_files: "No Files", 16 | files_count_and_size: "{count} files, {totalSize} in total", 17 | files_count_and_size_narrow: "{count} files\n{totalSize} in total", 18 | files_count_and_size_with_uploading: "{count} files, {totalSize} in total, {uploadingCount} uploading", 19 | files_count_and_size_with_uploading_narrow: "{count} files\n{totalSize} in total\n{uploadingCount} uploading", 20 | selected_files_count_and_size: "Selected {count} files, {totalSize} in total", 21 | selected_files_count_and_size_narrow: "Selected {count} files\n{totalSize} in total", 22 | upload: "Upload", 23 | confirm_override_question: "Be sure to override file(s) below?", 24 | confirm_override: "Confirm", 25 | progress_waiting: "Waiting", 26 | progress_uploading: "Uploading {progress}%", 27 | progress_retrying: "Retrying", 28 | progress_requesting: "Requesting", 29 | progress_error: "Error", 30 | progress_cancelled: "Cancelled", 31 | cancel_upload: "Cancel", 32 | back_to_problem: "Back to Problem", 33 | no_files_to_download: "No files to download.", 34 | invalid_filename: "Invalid filename.", 35 | error: { 36 | PERMISSION_DENIED: "Permission denied.", 37 | NO_SUCH_PROBLEM: "No such problem.", 38 | NO_SUCH_FILE: "No such file.", 39 | TOO_MANY_FILES: "Too many files. Please contact the administrator.", 40 | TOTAL_SIZE_TOO_LARGE: "Files total size too large. Please contact the administrator.", 41 | FILE_UUID_EXISTS: "Unknown error.", 42 | FILE_NOT_UPLOADED: "Couldn't find uploaded file." 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/problem_set.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Problem Set", 3 | error: { 4 | PERMISSION_DENIED: "Permission denied.", 5 | TAKE_TOO_MANY: "Requested number of items per page exceeds the limit." 6 | }, 7 | search_tag_placeholder: "Search tag ...", 8 | search_icon: { 9 | tag: "Tag", 10 | user: "Owner", 11 | nonpublic: "Nonpublic" 12 | }, 13 | search_filters: "Search filters", 14 | no_matching_tags: "No matching tags", 15 | no_tags: "No tags", 16 | show_tags: "Show Tags", 17 | manage_tags: "Manage Tags", 18 | add_problem: "Add Problem", 19 | no_problem: { 20 | message_search: "No matching problems", 21 | message_no_search: "No problems", 22 | back: "Back", 23 | clear_filters: "Clear Query", 24 | create: "Create" 25 | }, 26 | column_status: "Status", 27 | column_title: "Problem", 28 | column_submission_count: "Submissions", 29 | column_accepted_rate: "Acceptance", 30 | non_public: "Non-Public", 31 | no_title: "(No title)" 32 | }; 33 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/problem_tag_manager.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Tags Manage", 3 | tag_count: "{count} Tags", 4 | error: { 5 | PERMISSION_DENIED: "Permission denied.", 6 | NO_SUCH_PROBLEM_TAG: "No such problem tag." 7 | }, 8 | no_tags: "No Tags", 9 | new_tag: "New Tag", 10 | edit_tag: "Edit Tag", 11 | name_placeholder: "Tag name", 12 | default_language: "Default", 13 | add_language: "Add language", 14 | confirm_discard_unsaved: "Discard unsaved changes", 15 | new_tag_button: "New tag", 16 | submit: "Save", 17 | success_create: "Successfully created.", 18 | success_update: "Successfully saved.", 19 | confirm_delete_language: "Confirm delete language", 20 | confirm_delete_tag: "Confirm delete tag", 21 | close: "Close" 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/register.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Register", 3 | register_new_account: "Register new account", 4 | username: "Username", 5 | email: "E-mail", 6 | send_email_verification_code: "Send", 7 | email_verification_code_sent: "Email verification code sent.", 8 | email_verification_code: "E-mail Verify Code", 9 | password: "Password", 10 | retype_password: "Retype Password", 11 | register: "Register", 12 | already_have_account: "Already have an account? ", 13 | login: "Login", 14 | empty_username: "Please enter your username", 15 | invalid_username: 'Username should contains only A-Z, a-z, 0-9 and "-_.#$" and be in the length of 3 to 24', 16 | username_already_taken: "Username already taken", 17 | empty_email: "Please enter your email address", 18 | invalid_email: "Invalid email address", 19 | email_already_used: "Email already used", 20 | invalid_email_verification_code: "Invalid email verification code.", 21 | empty_password: "Please enter your password", 22 | invalid_password: "Password must be in the length of 6 to 32", 23 | passwords_do_not_match: "Passwords don't match", 24 | username_unavailable_message: "You can't register with this username.", 25 | email_unavailable_message: "You can't register with this email.", 26 | invalid_password_message: "Invalid password.", 27 | passwords_do_not_match_message: "Two passwords don't match.", 28 | errors: { 29 | ALREADY_LOGGEDIN: "You have already logged in.", 30 | DUPLICATE_USERNAME: "Username already taken.", 31 | DUPLICATE_EMAIL: "Email already used.", 32 | INVALID_EMAIL_VERIFICATION_CODE: "Invalid email verification code.", 33 | FAILED_TO_SEND: "Failed to send mail: {errorMessage}", 34 | RATE_LIMITED: "Your operations are too frequent. Please try again later." 35 | }, 36 | success: "Welcome, {username}!" 37 | }; 38 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/submission.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Submission", 3 | cancel: "Cancel", 4 | confirm_cancel: "Confirm cancel", 5 | success_cancel: "Successfully canceled.", 6 | rejudge: "Rejudge", 7 | confirm_rejudge: "Confirm rejudge", 8 | success_rejudge: "Successfully rejudged.", 9 | set_public: "Make Public", 10 | confirm_set_public: "Confirm make public", 11 | success_set_public: "Successfully made public.", 12 | set_non_public: "Make Non-public", 13 | confirm_set_non_public: "Confirm made non-public", 14 | success_set_non_public: "Successfully make non-public.", 15 | delete: "Delete", 16 | confirm_delete: "Confirm delete", 17 | success_delete: "Successfully deleted.", 18 | error: { 19 | NO_SUCH_PROBLEM: "No such problem.", 20 | NO_SUCH_FILE: "No such file.", 21 | NO_SUCH_SUBMISSION: "No such submission.", 22 | PERMISSION_DENIED: "Permission denied." 23 | }, 24 | failed_to_format: "Error formatting code: {error}", 25 | format_code: "Format code", 26 | show_original_code: "Show original code", 27 | compilation_message: "Compilation Message", 28 | system_message: "System Message", 29 | sample: "Samples", 30 | sample_testcase: "Sample", 31 | subtask: { 32 | title: "Subtask", 33 | score: "Score" 34 | }, 35 | testcase: { 36 | title: "Testcase", 37 | score: "Score", 38 | input: "Input file", 39 | output: "Output file", 40 | user_output: "Your output", 41 | user_error: "Your standard error output", 42 | checker_message: "Checker message", 43 | interactor_message: "Interactor message", 44 | system_message: "System message" 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/submission_item.js: -------------------------------------------------------------------------------- 1 | return { 2 | columns: { 3 | status: "Status", 4 | score: "Score", 5 | problem: "Problem", 6 | time: "Time", 7 | memory: "Memory", 8 | answer: "Answer", 9 | submitter: "Submitter", 10 | submit_time: "Submit Time" 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/submission_statistics.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Statistics", 3 | error: { 4 | NO_SUCH_PROBLEM: "No such problem.", 5 | PERMISSION_DENIED: "Permission denied.", 6 | TAKE_TOO_MANY: "Requested number of items per page exceeds the limit." 7 | }, 8 | 9 | header: "Accepted Submissions", 10 | type: { 11 | Fastest: "Fastest", 12 | MinMemory: "Min Memory", 13 | MinAnswerSize: "Shortest", 14 | Earliest: "Earliest" 15 | }, 16 | empty: "No submissions", 17 | empty_goback: "Go back", 18 | header_score_distribution: "Score Distribution", 19 | header_score_prefix_sum: "Prefix Sum", 20 | header_score_suffix_sum: "Suffix Sum", 21 | chart_tooltip: { 22 | score: "Score: ", 23 | count: "Count: " 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/submissions.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Submissions", 3 | query: { 4 | problem_id: "Problem", 5 | submitter: "Submitter", 6 | code_language: "Language", 7 | code_language_all: "All", 8 | status: "Status", 9 | status_all: "All", 10 | filter: "Filter", 11 | my_submissions: "My submissions" 12 | }, 13 | query_error: { 14 | INVALID_PROBLEM_ID: "Invalid problem ID.", 15 | INVALID_USERNAME: "Invalid username.", 16 | NO_SUCH_PROBLEM: "No such problem.", 17 | NO_SUCH_USER: "No such user." 18 | }, 19 | empty: { 20 | message_filtered: "No matching submissions", 21 | message_not_filtered: "No submissions", 22 | goback: "Go back" 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/user.js: -------------------------------------------------------------------------------- 1 | return { 2 | error: { 3 | NO_SUCH_USER: "No such user." 4 | }, 5 | edit_profile: "Edit profile", 6 | joined: "Joined ", 7 | social: { 8 | email: "E-mail", 9 | qq: "QQ", 10 | telegram: "Telegram", 11 | github: "GitHub" 12 | }, 13 | subway_graph: { 14 | start_of_week: "7", 15 | week: { 16 | 1: "Mon", 17 | 2: "Tue", 18 | 3: "Wed", 19 | 4: "Thu", 20 | 5: "Fri", 21 | 6: "Sat", 22 | 7: "Sun" 23 | }, 24 | month: { 25 | 1: "Jan", 26 | 2: "Feb", 27 | 3: "Mar", 28 | 4: "Apr", 29 | 5: "May", 30 | 6: "Jun", 31 | 7: "Jul", 32 | 8: "Aug", 33 | 9: "Sep", 34 | 10: "Oct", 35 | 11: "Nov", 36 | 12: "Dec" 37 | }, 38 | popup: { 39 | submission: "{count} submission", 40 | submissions: "{count} submissions", 41 | no_submissions: "No submissions" 42 | }, 43 | link: "Search this user's submissions", 44 | legend: { 45 | less: "Less", 46 | more: "More" 47 | } 48 | }, 49 | statictics: { 50 | ac_count: "AC. Problems", 51 | contest_take_part_count: "Participation", 52 | rating: "Rating", 53 | rank: "Rank" 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/locales/messages/en-US/users.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "Users", 3 | error: { 4 | TAKE_TOO_MANY: "Requested number of items per page exceeds the limit." 5 | }, 6 | manage_groups: "Manage groups", 7 | rank: "Rank", 8 | username: "Username", 9 | bio: "Bio", 10 | accepted_problem_count: "AC. Count", 11 | rating: "Rating" 12 | }; 13 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP.ts: -------------------------------------------------------------------------------- 1 | import.meta.compileTime("./ja-JP/import.ts"); 2 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/common.js: -------------------------------------------------------------------------------- 1 | return { 2 | navbar: { 3 | home: "ホーム", 4 | problem_set: "問題", 5 | contests: "コンテスト", 6 | submissions: "提出一覧", 7 | members: "ユーザ", 8 | discussion: "ディスカッション" 9 | }, 10 | header: { 11 | user: { 12 | login: "ログイン", 13 | register: "新規登録", 14 | profile: "マイページ", 15 | submissions: "自分の提出", 16 | problems: "自分の問題", 17 | discussions: "[TBT] My discussions", 18 | groups: "[TBT] My groups", 19 | edit_profile: "プロファイル設定", 20 | preference: "環境設定", 21 | logout: "ログアウト" 22 | } 23 | }, 24 | toast: { 25 | success: "成功", 26 | info: "ヒント", 27 | warning: "警告", 28 | error: "エラー" 29 | }, 30 | localized_content_unavailable: "設定された言語では表示できないため,{display_locale}で表示します。", 31 | footer: { 32 | judge_machine: "ジャッジサーバーの状態", 33 | locale: "[TBT] Language", 34 | github: "[TBT] Open source" 35 | }, 36 | invalid_url: "[TBT] Invalid URL.", 37 | request_error: { 38 | 400: "[TBT] Invalid request.", 39 | 401: "[TBT] Verification failed. Please refresh and try again.", 40 | 429: "[TBT] Too many requests. Please try again later.", 41 | 500: "[TBT] Server error. Please try again later.", 42 | 502: "[TBT] Couldn't connect to the server. Please try again later.", 43 | 503: "[TBT] Couldn't connect to the server. Please try again later.", 44 | 504: "[TBT] Timeout connecting to the server. Please try again later.", 45 | unknown: "[TBT] Request error: {text}" 46 | }, 47 | confirm_navigation: "[TBT] Be sure to leave this page? Your changes will not be saved.", 48 | recaptcha: { 49 | copyright: 50 | '[TBT] This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.' 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/components/code_box.js: -------------------------------------------------------------------------------- 1 | return { 2 | omitted: "[TBT] {count} byte omitted", 3 | omitted_s: "[TBT] {count} bytes omitted" 4 | }; 5 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/components/permission_manager.js: -------------------------------------------------------------------------------- 1 | return { 2 | header: "権限管理", 3 | owner: "オーナー", 4 | permission_for_users: "ユーザ", 5 | search_users: "ユーザ名を検索して権限を与える …", 6 | search_users_no_result: "制限を満たすユーザがいません。", 7 | permission_for_groups: "ユーザ グループ", 8 | no_group_granted: "管理者権限を与えたユーザ グループはありません", 9 | search_groups: "ユーザ グループ名を検索して権限を与える …", 10 | search_groups_no_result: "[TBT] No matching groups.", 11 | submit: "提出", 12 | no_submit_permission: "提出の権限を持っていません", 13 | cancel: "キャンセル", 14 | confirm_cancel: "キャンセル", 15 | submit_error: { 16 | PERMISSION_DENIED: "権限がありません。", 17 | NO_SUCH_USER: " ID が {id} のユーザは存在しません。", 18 | NO_SUCH_GROUP: " ID が {id} のユーザ グループは存在しません。" 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/components/problem_search.js: -------------------------------------------------------------------------------- 1 | return { 2 | placeholder: "問題名 / ID …", 3 | no_result: "制限を満たす問題はありません。" 4 | }; 5 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/components/user_search.js: -------------------------------------------------------------------------------- 1 | return { 2 | placeholder: "ユーザ名 …", 3 | no_result: "制限を満たすユーザがいません。" 4 | }; 5 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/discussion_edit.js: -------------------------------------------------------------------------------- 1 | return { 2 | title_new: "[TBT] Add discussion", 3 | title_update: "[TBT] Edit discussion", 4 | errors: { 5 | PERMISSION_DENIED: "[TBT] Permission denied.", 6 | NO_SUCH_PROBLEM: "[TBT] No such problem.", 7 | NO_SUCH_DISCUSSION: "[TBT] No such discussion." 8 | }, 9 | header: { 10 | add: "[TBT] Add discussion", 11 | update: "[TBT] Edit discussion" 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/discussions.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "[TBT] Discussions", 3 | error: { 4 | PERMISSION_DENIED: "[TBT] Permission denied.", 5 | NO_SUCH_PROBLEM: "[TBT] No such problem.", 6 | NO_SUCH_USER: "[TBT] No such user.", 7 | TAKE_TOO_MANY: "[TBT] Requested number of items per page exceeds the limit." 8 | }, 9 | breadcrumb: { 10 | discussion: "[TBT] Discussion", 11 | general: "[TBT] General", 12 | problem: "[TBT] Problem" 13 | }, 14 | add_discussion: "[TBT] Add discussion", 15 | search_discussion: { 16 | placeholder: "[TBT] Title ...", 17 | no_result: "[TBT] No matching discussions." 18 | }, 19 | search_icon: { 20 | user: "[TBT] Publisher", 21 | nonpublic: "[TBT] Nonpublic" 22 | }, 23 | search_filters: "[TBT] Search filters", 24 | no_discussions: { 25 | message_search: "[TBT] No matching discussions", 26 | message_no_search: "[TBT] No discussions", 27 | back: "[TBT] Back", 28 | clear_filters: "[TBT] Clear Query", 29 | create: "[TBT] Add discussion" 30 | }, 31 | column_title: "[TBT] Title", 32 | column_problem: "[TBT] Problem", 33 | column_publisher: "[TBT] Publisher", 34 | column_reply_count: "[TBT] Replies", 35 | column_sort_time: "[TBT] Last updated", 36 | non_public: "[TBT] Nonpublic", 37 | no_title: "[TBT] (No title)" 38 | }; 39 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/error.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "エラー", 3 | error: "エラーが発生しました", 4 | unexpected_error: "予期せぬエラーが発生しました", 5 | back: "戻る", 6 | refresh: "再読み込み" 7 | }; 8 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/forgot.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "[TBT] Reset password", 3 | reset_your_password: "[TBT] Reset your password", 4 | email: "メールアドレス", 5 | send_email_verification_code: "確認コードを送信", 6 | email_verification_code_sent: "[TBT] Email verification code sent.", 7 | email_verification_code: "確認コード", 8 | password: "パスワード", 9 | retype_password: "パスワードの再入力", 10 | submit: "[TBT] Submit", 11 | success: "[TBT] Password reset", 12 | errors: { 13 | ALREADY_LOGGEDIN: "すでにログインしています。", 14 | NO_SUCH_USER: "ユーザは存在しません。", 15 | INVALID_EMAIL_VERIFICATION_CODE: "[TBT] Invalid email verification code.", 16 | FAILED_TO_SEND: "[TBT] Failed to send mail: {errorMessage}", 17 | RATE_LIMITED: "[TBT] Your operations are too frequent. Please try again later." 18 | }, 19 | empty_email: "メールアドレスを入力してください", 20 | invalid_email: "無効なメールアドレスです", 21 | invalid_email_verification_code: "[TBT] Invalid email verification code.", 22 | empty_password: "パスワードを入力してください", 23 | invalid_password: "パスワードの長さは 6 文字から 32 文字にしてください", 24 | passwords_do_not_match: "パスワードが一致しません", 25 | email_invalid_message: "Invalid email.", 26 | invalid_password_message: "無効なパスワードです。", 27 | passwords_do_not_match_message: "パスワードが一致しません。" 28 | }; 29 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/groups.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "[TBT] Groups", 3 | header: "[TBT] Groups", 4 | not_logged_in: "[TBT] Not logged in.", 5 | no_groups: "[TBT] No Groups", 6 | group_admin: "[TBT] Group admin", 7 | errors: { 8 | PERMISSION_DENIED: "[TBT] Permission denied.", 9 | NO_SUCH_GROUP: "[TBT] No such group.", 10 | NO_SUCH_USER: "[TBT] No such user", 11 | USER_NOT_IN_GROUP: "[TBT] This user is not in the group.", 12 | USER_ALREADY_IN_GROUP: "[TBT] This user is already in the group.", 13 | DUPLICATE_GROUP_NAME: "[TBT] The group name has been taken.", 14 | GROUP_ADMIN_CAN_NOT_BE_REMOVED: "[TBT] Group admin can not be removed." 15 | }, 16 | create_group: "[TBT] Create group", 17 | create_group_name: "[TBT] Group name", 18 | confirm_create_group: "[TBT] Create", 19 | search_to_add_user: "[TBT] Add user ...", 20 | rename_group: "[TBT] Rename", 21 | rename_group_new_name: "[TBT] New name", 22 | confirm_rename_group: "[TBT] Rename", 23 | delete_group: "[TBT] Delete", 24 | confirm_delete_group: "[TBT] Confirm delete group", 25 | remove_member: "[TBT] Remove", 26 | confirm_remove_member: "[TBT] Confirm remove member", 27 | invalid_group_name: "[TBT] Invalid group name." 28 | }; 29 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/home.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "ホーム", 3 | annnouncements: { 4 | header: "[TBT] Annnouncement", 5 | title: "[TBT] Title", 6 | date: "[TBT] Date", 7 | no_annnouncements: "[TBT] No Annnouncements" 8 | }, 9 | latest_problems: { 10 | header: "[TBT] Latest Problems", 11 | status: "[TBT] Status", 12 | problem: "[TBT] Problem", 13 | updated_time: "[TBT] Updated Time", 14 | no_problems: "[TBT] No Problems" 15 | }, 16 | hitokoto: { 17 | header: "[TBT] Hitokoto (ヒトコト)", 18 | error: "[TBT] Error loading Hitokoto.", 19 | refresh: "[TBT] Refresh" 20 | }, 21 | countdown: { 22 | header: "[TBT] Countdown", 23 | display_time_first: "[TBT] 1", 24 | before_time: "[TBT] ", 25 | after_time_before_event: "[TBT] before ", 26 | after_days_before_event: "[TBT] days before ", 27 | after_event: "[TBT] ", 28 | completed_before_event: "[TBT] ", 29 | completed_after_event: "[TBT] has started" 30 | }, 31 | search_problem: "[TBT] Search Problem", 32 | top_users: { 33 | header: "[TBT] Top Users", 34 | username: "[TBT] Username", 35 | bio: "[TBT] Bio", 36 | rating: "[TBT] Rating", 37 | accepted_problem_count: "[TBT] AC. Count", 38 | no_users: "[TBT] No Users" 39 | }, 40 | friend_links: "[TBT] Friend Links" 41 | }; 42 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/home_settings.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "[TBT] Homepage Settings", 3 | errors: { 4 | PERMISSION_DENIED: "[TBT] Permission denied.", 5 | NO_SUCH_DISCUSSION: "[TBT] No such discussion with ID {id}." 6 | }, 7 | header: "[TBT] Homepage Settings", 8 | set_default_locale: "[TBT] Set default language", 9 | delete_locale: "[TBT] Delete language", 10 | confirm_delete_locale: "[TBT] Confirm delete language", 11 | notice: { 12 | header: "[TBT] Notice", 13 | placeholder: "[TBT] Notice text ..." 14 | }, 15 | annnouncements: { 16 | header: "[TBT] Annnouncements", 17 | title: "[TBT] Title", 18 | date: "[TBT] Date", 19 | operations: "[TBT] Operations", 20 | add: "[TBT] Add annnouncement" 21 | }, 22 | hitokoto: "[TBT] Hitokoto", 23 | countdown: "[TBT] Countdown", 24 | friend_links: "[TBT] Friend Links", 25 | submit: "[TBT] Submit", 26 | success: "[TBT] Successfully updated." 27 | }; 28 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/import.ts: -------------------------------------------------------------------------------- 1 | import importMessages from "../importMessages"; 2 | export default () => importMessages(import.meta.url); 3 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/judge_machine.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "ジャッジサーバー", 3 | header: "ジャッジサーバー", 4 | refresh: "更新", 5 | add: "追加", 6 | add_new_name: "ジャッジサーバーの名前", 7 | confirm_add: "追加", 8 | add_success: "追加に成功しました。", 9 | status: "状態", 10 | name: "名前", 11 | cpu: "CPU", 12 | memory: "メモリー", 13 | kernel: "カーネル", 14 | operations: "操作", 15 | online: "オンライン", 16 | offline: "オフライン", 17 | key: "キー", 18 | reset_key: "キーを変更", 19 | confirm_reset_key: "変更する", 20 | reset_key_success: "変更に成功しました。", 21 | delete: "削除", 22 | confirm_delete: "削除する", 23 | delete_success: "削除に成功しました。", 24 | error: { 25 | PERMISSION_DENIED: "権限がありません。", 26 | NO_SUCH_JUDGE_CLIENT: "このジャッジサーバーは存在しません。" 27 | }, 28 | no_judge_machine: "ジャッジサーバーは現在ありません。" 29 | }; 30 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/language.js: -------------------------------------------------------------------------------- 1 | return { 2 | zh_CN: "中国語(簡体)", 3 | en_US: "英語", 4 | ja_JP: "日本語" 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/login.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "ログイン", 3 | login_to_your_account: "ログイン", 4 | username_or_email: "ユーザ名 / メールアドレス", 5 | password: "パスワード", 6 | forgot_password: "[TBT] Forgot password", 7 | remember: "ログインの状態を保つ", 8 | forget: "パスワードを忘れた方はこちら", 9 | new_user: "アカウントを登録しましたか?", 10 | register: "新規登録", 11 | login: "ログイン", 12 | empty_username_or_email: "[TBT] Please enter your username or email", 13 | invalid_username_or_email: "[TBT] Invalid username or email", 14 | empty_password: "パスワードを入力してください", 15 | no_such_user: "ユーザは存在しません。", 16 | wrong_password: "パスワードが間違っています", 17 | welcome: "お帰りなさい,{username} さん!", 18 | errors: { 19 | NO_SUCH_USER: "ユーザは存在しません。", 20 | ALREADY_LOGGEDIN: "すでにログインしています。", 21 | ALREADY_MIGRATED: "[TBT] System error. Please try again later.", 22 | DUPLICATE_USERNAME: "このユーザ名はすでに使用されています。" 23 | }, 24 | migration: { 25 | title: "[TBT] Choose a new username", 26 | message: 27 | "[TBT] We are sorry but your username is invalid in our new system. You must choose a new username to continue.", 28 | message_username: "[TBT] ユーザ名は 3 文字から 24 文字で,英数字と -_.#$ が使用できます。", 29 | invalid_username: "[TBT] Invalid username.", 30 | placeholder: "[TBT] New username", 31 | confirm: "[TBT] Rename", 32 | cancel: "[TBT] Cancel" 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/problem_edit.js: -------------------------------------------------------------------------------- 1 | return { 2 | title_edit: "問題編集", 3 | title_new: "新規作成", 4 | header_edit: "問題 {idString} を編集する", 5 | header_new: "問題を新規作成", 6 | back_to_problem: "戻る", 7 | confirm_back_to_problem: "保存せず戻る", 8 | submit: "保存", 9 | no_submit_permission: "権限がありません", 10 | submit_success: "保存に成功しました。", 11 | something_empty: "[TBT] Please fill all titles and section contents.", 12 | error: { 13 | FAILED: "失敗しました。", 14 | NO_SUCH_PROBLEM: "問題がありません。", 15 | PERMISSION_DENIED: "権限がありません。" 16 | }, 17 | header_samples: "サンプル", 18 | header_tags: "タグ", 19 | tags_placeholder: "タグを検索する …", 20 | no_addable_tags: "制限を満たすタグは存在しません。", 21 | content_editor: { 22 | title: "タイトル", 23 | preview_all: "プレビュー", 24 | default: "デフォルト言語", 25 | add_default_sections: "[TBT] Add default sections", 26 | confirm_delete: "削除する", 27 | section_title: "セクションのタイトル", 28 | preview: "プレビュー", 29 | add_section: { 30 | before_this_section: "前に追加", 31 | after_this_section: "後に追加" 32 | }, 33 | section_content: "セクションの内容", 34 | new_sample: "サンプルを追加します", 35 | sample_input: "入力例", 36 | sample_output: "出力例", 37 | sample_explanation: "サンプルの説明", 38 | section_options: { 39 | move_up: "上に移動", 40 | move_down: "下に移動", 41 | delete: "削除", 42 | confirm_delete: "削除する" 43 | }, 44 | section_type: { 45 | text: "テキスト", 46 | sample: "サンプル" 47 | } 48 | }, 49 | sample_editor: { 50 | add_sample_when_empty: "サンプルを追加する", 51 | sample_id: "サンプル番号", 52 | warning: { 53 | not_referenced: "{language} で参照されていません", 54 | multiple_references: "{language} で {referenceCount} 回参照されています" 55 | }, 56 | add_sample: { 57 | before_this_sample: "前に追加", 58 | after_this_sample: "後に追加" 59 | }, 60 | options: { 61 | move_up: "上に移動", 62 | move_down: "下に移動", 63 | delete: "削除", 64 | confirm_delete: "削除する" 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/problem_files.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "問題のファイル", 3 | header_testdata: "データ", 4 | header_additional_files: "他のファイル", 5 | filename: "ファイル名", 6 | size: "サイズ", 7 | operations_and_status: "操作 / 状態", 8 | operations: "操作", 9 | delete: "削除", 10 | download_as_archive: "ダウンロード", 11 | download_as_archive_error: "ファイル {filename} のダウンロードに失敗しました:{error}", 12 | confirm_delete: "削除する", 13 | new_filename: "新しいファイル名", 14 | rename: "名前を変更する", 15 | no_files: "ファイルはありません", 16 | files_count_and_size: "{count} 個, {totalSize}", 17 | files_count_and_size_narrow: "{count} 個\n{totalSize}", 18 | files_count_and_size_with_uploading: "{count} 個, {totalSize}, {uploadingCount} 個アップロード中", 19 | files_count_and_size_with_uploading_narrow: "{count} 個\n{totalSize}\n{uploadingCount} 個アップロード中", 20 | selected_files_count_and_size: "{count} 個, {totalSize} 選択中", 21 | selected_files_count_and_size_narrow: "{count} 個, {totalSize} 選択中", 22 | upload: "アップロード", 23 | confirm_override_question: "以下のファイルを上書きしますか?", 24 | confirm_override: "上書きする", 25 | progress_waiting: "アップロードを待機中", 26 | progress_uploading: "アップロード中 {progress}%", 27 | progress_retrying: "[TBT] Retrying", 28 | progress_requesting: "リクエストしています", 29 | progress_error: "アップロードに失敗しました", 30 | progress_cancelled: "アップロードをキャンセルしました", 31 | cancel_upload: "キャンセル", 32 | back_to_problem: "戻る", 33 | no_files_to_download: "[TBT] No files to download.", 34 | invalid_filename: "[TBT] Invalid filename.", 35 | error: { 36 | PERMISSION_DENIED: "権限がありません。", 37 | NO_SUCH_PROBLEM: "問題が存在しません。", 38 | NO_SUCH_FILE: "ファイルが存在しません。", 39 | TOO_MANY_FILES: "ファイル数が制限を超えました,管理者に連絡してください。", 40 | TOTAL_SIZE_TOO_LARGE: "データのサイズが制限を超えました,管理者に連絡してください。", 41 | FILE_UUID_EXISTS: "[TBT] Unknown error.", 42 | FILE_NOT_UPLOADED: "アップロードしたファイルは存在しません。" 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/problem_set.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "問題", 3 | error: { 4 | PERMISSION_DENIED: "権限がありません。", 5 | TAKE_TOO_MANY: "リクエストが制限を超えています。" 6 | }, 7 | search_tag_placeholder: "タグ …", 8 | search_icon: { 9 | tag: "タグ", 10 | user: "アップロード者", 11 | nonpublic: "非公開" 12 | }, 13 | search_filters: "フィルタ検索", 14 | no_matching_tags: "マッチするタグは存在しません", 15 | no_tags: "タグはありません", 16 | show_tags: "タグを表示する", 17 | manage_tags: "タグ管理", 18 | add_problem: "問題を追加する", 19 | no_problem: { 20 | message_search: "制限を満たす問題はありません", 21 | message_no_search: "問題はありません", 22 | back: "戻る", 23 | clear_filters: "フィルタを消去", 24 | create: "問題を追加" 25 | }, 26 | column_status: "[TBT] Status", 27 | column_title: "問題", 28 | column_submission_count: "提出", 29 | column_accepted_rate: "正答率", 30 | non_public: "非公開", 31 | no_title: "[TBT] (No title)" 32 | }; 33 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/problem_tag_manager.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "タグ管理", 3 | tag_count: "タグが {count} 個あります", 4 | error: { 5 | PERMISSION_DENIED: "権限がありません。", 6 | NO_SUCH_PROBLEM_TAG: "タグが存在しません。" 7 | }, 8 | no_tags: "タグがありません", 9 | new_tag: "タグを追加", 10 | edit_tag: "タグを変更", 11 | name_placeholder: "タグ名", 12 | default_language: "デフォルト", 13 | add_language: "言語を追加する", 14 | confirm_discard_unsaved: "キャンセル", 15 | new_tag_button: "タグを追加", 16 | submit: "保存", 17 | success_create: "追加に成功しました。", 18 | success_update: "保存に成功しました。", 19 | confirm_delete_language: "削除する", 20 | confirm_delete_tag: "削除する", 21 | close: "閉じる" 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/register.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "新規登録", 3 | register_new_account: "新規登録", 4 | username: "ユーザ名", 5 | email: "メールアドレス", 6 | send_email_verification_code: "確認コードを送信", 7 | email_verification_code_sent: "[TBT] Email verification code sent.", 8 | email_verification_code: "確認コード", 9 | password: "パスワード", 10 | retype_password: "パスワードの再入力", 11 | register: "新規登録", 12 | already_have_account: "もうアカウントを持っていますか?", 13 | login: "ログイン", 14 | empty_username: "ユーザ名を入力してください", 15 | invalid_username: "ユーザ名は 3 文字から 24 文字で,英数字と -_.#$ が使用できます。", 16 | username_already_taken: "このユーザ名はすでに使用されています", 17 | empty_email: "メールアドレスを入力してください", 18 | invalid_email: "無効なメールアドレスです", 19 | email_already_used: "このメールアドレスはすでに使用されています", 20 | invalid_email_verification_code: "[TBT] Invalid email verification code.", 21 | empty_password: "パスワードを入力してください", 22 | invalid_password: "パスワードの長さは 6 文字から 32 文字にしてください", 23 | passwords_do_not_match: "パスワードが一致しません", 24 | username_unavailable_message: "無効なユーザ名です。", 25 | email_unavailable_message: "無効なメールアドレスです。", 26 | invalid_password_message: "無効なパスワードです。", 27 | passwords_do_not_match_message: "パスワードが一致しません。", 28 | errors: { 29 | ALREADY_LOGGEDIN: "すでにログインしています。", 30 | DUPLICATE_USERNAME: "このユーザ名はすでに存在します。", 31 | DUPLICATE_EMAIL: "このメールアドレスはすでに使用されています。", 32 | INVALID_EMAIL_VERIFICATION_CODE: "[TBT] Invalid email verification code.", 33 | FAILED_TO_SEND: "[TBT] Failed to send mail: {errorMessage}", 34 | RATE_LIMITED: "[TBT] Your operations are too frequent. Please try again later." 35 | }, 36 | success: "新規登録ができました,{username} さん!" 37 | }; 38 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/submission.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "提出", 3 | cancel: "中止", 4 | confirm_cancel: "中止する", 5 | success_cancel: "ジャッジの中止に成功しました。", 6 | rejudge: "リジャッジ", 7 | confirm_rejudge: "リジャッジする", 8 | success_rejudge: "リジャッジに成功しました。", 9 | set_public: "[TBT] Make Public", 10 | confirm_set_public: "[TBT] Confirm make public", 11 | success_set_public: "[TBT] Successfully made public.", 12 | set_non_public: "[TBT] Make Non-public", 13 | confirm_set_non_public: "[TBT] Confirm made non-public", 14 | success_set_non_public: "[TBT] Successfully make non-public.", 15 | delete: "[TBT] Delete", 16 | confirm_delete: "[TBT] Confirm delete", 17 | success_delete: "[TBT] Successfully deleted.", 18 | error: { 19 | NO_SUCH_PROBLEM: "問題が存在しません。", 20 | NO_SUCH_FILE: "ファイルが存在しません。", 21 | NO_SUCH_SUBMISSION: "提出が存在しません。", 22 | PERMISSION_DENIED: "権限がありません。" 23 | }, 24 | failed_to_format: "コードのフォーマットに失敗しました:{error}", 25 | format_code: "コードをフォーマットする", 26 | show_original_code: "オリジナルを表示", 27 | compilation_message: "コンパイルメッセージ", 28 | system_message: "システムメッセージ", 29 | sample: "サンプル", 30 | sample_testcase: "サンプル", 31 | subtask: { 32 | title: "サブタスク", 33 | score: "得点:" 34 | }, 35 | testcase: { 36 | title: "テストケース", 37 | score: "得点:", 38 | input: "入力", 39 | output: "想定解", 40 | user_output: "出力", 41 | user_error: "エラー出力", 42 | checker_message: "ジャッジのメッセージ", 43 | interactor_message: "[TBT] Interactor message", 44 | system_message: "システムメッセージ" 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/submission_item.js: -------------------------------------------------------------------------------- 1 | return { 2 | columns: { 3 | status: "状態", 4 | score: "得点", 5 | problem: "問題", 6 | time: "実行時間", 7 | memory: "メモリ", 8 | answer: "コード", 9 | submitter: "ユーザ", 10 | submit_time: "提出日時" 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/submission_statistics.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "統計", 3 | error: { 4 | NO_SUCH_PROBLEM: "問題がありません。", 5 | PERMISSION_DENIED: "権限がありません。", 6 | TAKE_TOO_MANY: "リクエストが制限を超えています。" 7 | }, 8 | header: "満点の提出", 9 | type: { 10 | Fastest: "実行時間順", 11 | MinMemory: "使用メモリ順", 12 | MinAnswerSize: "コード長順", 13 | Earliest: "提出日時順" 14 | }, 15 | empty: "提出はありません", 16 | empty_goback: "戻る", 17 | header_score_distribution: "得点分布", 18 | header_score_prefix_sum: "prefix sum", 19 | header_score_suffix_sum: "suffix sum", 20 | chart_tooltip: { 21 | score: "得点:", 22 | count: "個数:" 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/submissions.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "提出一覧", 3 | query: { 4 | problem_id: "問題", 5 | submitter: "提出者", 6 | code_language: "言語", 7 | code_language_all: "全ての言語", 8 | status: "結果", 9 | status_all: "全ての結果", 10 | filter: "検索", 11 | my_submissions: "自分の提出" 12 | }, 13 | query_error: { 14 | INVALID_PROBLEM_ID: "問題 ID が無効です。", 15 | INVALID_USERNAME: "ユーザ名が無効です。", 16 | NO_SUCH_PROBLEM: "問題が存在しません。", 17 | NO_SUCH_USER: "このユーザ名は存在しません。" 18 | }, 19 | empty: { 20 | message_filtered: "制限を満たす提出はありません", 21 | message_not_filtered: "提出はありません", 22 | goback: "戻る" 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/user.js: -------------------------------------------------------------------------------- 1 | return { 2 | error: { 3 | NO_SUCH_USER: "ユーザが存在しません。" 4 | }, 5 | edit_profile: "プロファイル設定", 6 | joined: "入会日時 ", 7 | social: { 8 | email: "メールアドレス", 9 | qq: "QQ", 10 | telegram: "Telegram", 11 | github: "GitHub" 12 | }, 13 | subway_graph: { 14 | start_of_week: "1", 15 | week: { 16 | 1: "月", 17 | 2: "火", 18 | 3: "水", 19 | 4: "木", 20 | 5: "金", 21 | 6: "土", 22 | 7: "日" 23 | }, 24 | month: { 25 | 1: "1 月", 26 | 2: "2 月", 27 | 3: "3 月", 28 | 4: "4 月", 29 | 5: "5 月", 30 | 6: "6 月", 31 | 7: "7 月", 32 | 8: "8 月", 33 | 9: "9 月", 34 | 10: "10 月", 35 | 11: "11 月", 36 | 12: "12 月" 37 | }, 38 | popup: { 39 | submission: "{count} 回提出", 40 | submissions: "{count} 回提出", 41 | no_submissions: "提出はありません" 42 | }, 43 | link: "ユーザの提出", 44 | legend: { 45 | less: "少", 46 | more: "多" 47 | } 48 | }, 49 | statictics: { 50 | ac_count: "解いた問題数", 51 | contest_take_part_count: "コンテスト参加回数", 52 | rating: "レーティング", 53 | rank: "ランキング" 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/locales/messages/ja-JP/users.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "ユーザ", 3 | error: { 4 | TAKE_TOO_MANY: "リクエストが制限を超えています。" 5 | }, 6 | manage_groups: "[TBT] Manage groups", 7 | rank: "ランキング", 8 | username: "ユーザ名", 9 | bio: "自己紹介", 10 | accepted_problem_count: "解いた問題数", 11 | rating: "レーティング" 12 | }; 13 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import.meta.compileTime("./zh-CN/import.ts"); 2 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/common.js: -------------------------------------------------------------------------------- 1 | return { 2 | navbar: { 3 | home: "首页", 4 | problem_set: "题库", 5 | contests: "比赛", 6 | submissions: "评测", 7 | members: "用户", 8 | discussion: "讨论" 9 | }, 10 | header: { 11 | user: { 12 | login: "登录", 13 | register: "注册", 14 | profile: "我的资料", 15 | submissions: "我的提交", 16 | problems: "我的题目", 17 | discussions: "我的讨论", 18 | groups: "我的用户组", 19 | edit_profile: "编辑资料", 20 | preference: "偏好设置", 21 | logout: "注销" 22 | } 23 | }, 24 | toast: { 25 | success: "成功", 26 | info: "提示", 27 | warning: "警告", 28 | error: "错误" 29 | }, 30 | localized_content_unavailable: "该内容没有您偏好的语言版本,已为您显示{display_locale}版本。", 31 | footer: { 32 | judge_machine: "评测机状态", 33 | locale: "语言", 34 | github: "开源项目" 35 | }, 36 | invalid_url: "无效的 URL。", 37 | request_error: { 38 | 400: "无效的请求。", 39 | 401: "验证失败,请刷新后重试。", 40 | 429: "请求过于频繁,请稍后重试。", 41 | 500: "服务器错误,请稍后重试。", 42 | 502: "无法连接到服务器,请稍后重试。", 43 | 503: "无法连接到服务器,请稍后重试。", 44 | 504: "连接服务器超时,请稍后重试。", 45 | unknown: "请求出错:{text}" 46 | }, 47 | confirm_navigation: "确定要离开当前页面吗?您的修改将不被保存。", 48 | recaptcha: { 49 | copyright: 50 | '本网站受 reCAPTCHA 和 Google 的保护,适用隐私政策服务条款。' 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/components/code_box.js: -------------------------------------------------------------------------------- 1 | return { 2 | omitted: "已省略 {count} 字节", 3 | omitted_s: "已省略 {count} 字节" 4 | }; 5 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/components/permission_manager.js: -------------------------------------------------------------------------------- 1 | return { 2 | header: "权限管理", 3 | owner: "所有者", 4 | permission_for_users: "用户", 5 | search_users: "搜索用户名以添加用户 …", 6 | search_users_no_result: "没有符合条件的用户。", 7 | permission_for_groups: "用户组", 8 | no_group_granted: "暂无用户组被授权", 9 | search_groups: "搜索组名以添加用户组 …", 10 | search_groups_no_result: "没有符合条件的用户组。", 11 | submit: "提交", 12 | no_submit_permission: "无提交权限", 13 | cancel: "取消", 14 | confirm_cancel: "确认取消", 15 | submit_error: { 16 | PERMISSION_DENIED: "权限不足。", 17 | NO_SUCH_USER: "找不到 ID 为 {id} 的用户。", 18 | NO_SUCH_GROUP: "找不到 ID 为 {id} 的用户组。" 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/components/problem_search.js: -------------------------------------------------------------------------------- 1 | return { 2 | placeholder: "题目名 / ID …", 3 | no_result: "没有符合条件的题目。" 4 | }; 5 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/components/user_search.js: -------------------------------------------------------------------------------- 1 | return { 2 | placeholder: "用户名 …", 3 | no_result: "没有符合条件的用户。" 4 | }; 5 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/discussion.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "讨论", 3 | errors: { 4 | PERMISSION_DENIED: "权限不足。", 5 | NO_SUCH_DISCUSSION: "无此讨论。", 6 | NO_SUCH_DISCUSSION_REPLY: "无此回复。", 7 | INVALID_EMOJI: "无效的 Emoji。" 8 | }, 9 | reply_count_0: "暂无回复", 10 | reply_count: "{replyCount} 条回复", 11 | reply_count_s: "{replyCount} 条回复", 12 | add_discussion: "新建讨论", 13 | item: { 14 | commented_on: "发表于 ", 15 | edited: "已编辑", 16 | label: { 17 | nonpublic: "未公开", 18 | problem_owner: "题目上传者", 19 | discussion_publisher: "讨论发起者" 20 | }, 21 | actions: { 22 | edit: "编辑", 23 | quote: "引用", 24 | permission_manage: "管理权限", 25 | set_non_public: "取消公开", 26 | set_public: "设为公开", 27 | confirm_set_non_public: "确认取消公开", 28 | confirm_set_public: "确认设为公开", 29 | delete: "删除", 30 | confirm_delete: "确认删除", 31 | confirm_delete_dialog_title: "删除讨论", 32 | confirm_delete_dialog_content: "确定删除该讨论吗?所有回复也将一并删除。", 33 | confirm_delete_dialog_confirm: "确认删除", 34 | confirm_delete_dialog_cancel: "取消" 35 | } 36 | }, 37 | edit: { 38 | placeholder: { 39 | title: "标题", 40 | add_reply: "添加回复 …", 41 | add_discussion: "添加讨论 …", 42 | update_reply: "编辑回复 …", 43 | update_discussion: "编辑讨论 …" 44 | }, 45 | tabs: { 46 | edit: "编辑", 47 | preview: "预览" 48 | }, 49 | actions: { 50 | add_reply: "回复", 51 | add_discussion: "提交", 52 | update_reply: "修改回复", 53 | update_discussion: "提交", 54 | update_discussion_no_submit_permission: "无提交权限", 55 | cancel: "取消", 56 | confirm_cancel: "放弃修改并取消" 57 | } 58 | }, 59 | load_more: { 60 | hidden_count: "{count} 条回复未显示", 61 | hidden_count_s: "{count} 条回复未显示", 62 | load_more: "加载更多 …" 63 | }, 64 | custom_emoji: "自定义 Emoji", 65 | permission_manager_description: "讨论 #{id}", 66 | permission_level: { 67 | read: "只读", 68 | write: "读写" 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/discussion_edit.js: -------------------------------------------------------------------------------- 1 | return { 2 | title_new: "添加讨论", 3 | title_update: "编辑讨论", 4 | errors: { 5 | PERMISSION_DENIED: "权限不足。", 6 | NO_SUCH_PROBLEM: "无此题目。", 7 | NO_SUCH_DISCUSSION: "无此讨论。" 8 | }, 9 | header: { 10 | add: "添加讨论", 11 | update: "修改讨论" 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/discussions.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "讨论", 3 | error: { 4 | PERMISSION_DENIED: "权限不足。", 5 | NO_SUCH_PROBLEM: "无此题目。", 6 | NO_SUCH_USER: "无此用户。", 7 | TAKE_TOO_MANY: "请求每页项目数量超出限制。" 8 | }, 9 | breadcrumb: { 10 | discussion: "讨论", 11 | general: "综合", 12 | problem: "题目" 13 | }, 14 | add_discussion: "新建讨论", 15 | search_discussion: { 16 | placeholder: "标题 …", 17 | no_result: "没有符合条件的讨论。" 18 | }, 19 | search_icon: { 20 | tag: "标签", 21 | user: "题目拥有者", 22 | nonpublic: "未公开" 23 | }, 24 | search_filters: "搜索条件", 25 | no_discussions: { 26 | message_search: "找不到符合条件的讨论", 27 | message_no_search: "暂无任何讨论", 28 | back: "返回", 29 | clear_filters: "清除搜索条件", 30 | create: "发布讨论" 31 | }, 32 | column_title: "标题", 33 | column_problem: "所属题目", 34 | column_publisher: "发布者", 35 | column_reply_count: "回复数量", 36 | column_sort_time: "更新时间", 37 | non_public: "未公开", 38 | no_title: "(无标题)" 39 | }; 40 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/error.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "错误", 3 | error: "错误", 4 | unexpected_error: "发生了预料之外的错误", 5 | back: "返回", 6 | refresh: "刷新" 7 | }; 8 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/forgot.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "重置密码", 3 | reset_your_password: "重置您的密码", 4 | email: "邮箱", 5 | send_email_verification_code: "发送验证码", 6 | email_verification_code_sent: "邮箱验证码已发送。", 7 | email_verification_code: "邮箱验证码", 8 | password: "新密码", 9 | retype_password: "确认密码", 10 | submit: "提交", 11 | success: "密码已重置", 12 | errors: { 13 | ALREADY_LOGGEDIN: "你已经登陆过。", 14 | NO_SUCH_USER: "无此用户。", 15 | INVALID_EMAIL_VERIFICATION_CODE: "邮箱验证码无效。", 16 | FAILED_TO_SEND: "发送邮件失败:{errorMessage}", 17 | RATE_LIMITED: "您的操作过于频繁,请稍后再试。" 18 | }, 19 | empty_email: "邮箱不能为空", 20 | invalid_email: "邮箱无效", 21 | invalid_email_verification_code: "邮箱验证码无效", 22 | empty_password: "密码不能为空", 23 | invalid_password: "密码长度需要在 6 到 32 之间", 24 | passwords_do_not_match: "密码不一致", 25 | email_invalid_message: "邮箱无效。", 26 | invalid_password_message: "密码无效。", 27 | passwords_do_not_match_message: "两次输入的密码不一致。" 28 | }; 29 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/groups.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "用户组", 3 | header: "用户组", 4 | not_logged_in: "未登录。", 5 | no_groups: "暂无用户组", 6 | group_admin: "组管理员", 7 | errors: { 8 | PERMISSION_DENIED: "权限不足。", 9 | NO_SUCH_GROUP: "无此用户组。", 10 | NO_SUCH_USER: "无此用户。", 11 | USER_NOT_IN_GROUP: "该用户不在组中。", 12 | USER_ALREADY_IN_GROUP: "该用户已在组中。", 13 | DUPLICATE_GROUP_NAME: "该组名已被占用。", 14 | GROUP_ADMIN_CAN_NOT_BE_REMOVED: "组管理员不能被移除。" 15 | }, 16 | create_group: "创建组", 17 | create_group_name: "新组名", 18 | confirm_create_group: "创建", 19 | search_to_add_user: "添加用户 …", 20 | rename_group: "重命名", 21 | rename_group_new_name: "新组名", 22 | confirm_rename_group: "重命名", 23 | delete_group: "删除", 24 | confirm_delete_group: "确认删除组", 25 | remove_member: "移出组", 26 | confirm_remove_member: "确认移出组", 27 | invalid_group_name: "无效的组名。" 28 | }; 29 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/home.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "首页", 3 | annnouncements: { 4 | header: "公告", 5 | title: "标题", 6 | date: "日期", 7 | no_annnouncements: "暂无公告" 8 | }, 9 | latest_problems: { 10 | header: "最新题目", 11 | status: "状态", 12 | problem: "题目", 13 | updated_time: "更新时间", 14 | no_problems: "暂无题目" 15 | }, 16 | hitokoto: { 17 | header: "一言(ヒトコト)", 18 | error: "加载一言时发生错误。", 19 | refresh: "刷新" 20 | }, 21 | countdown: { 22 | header: "倒计时", 23 | display_time_first: "0", 24 | before_event: "距离 ", 25 | after_event_before_time: " 还有 ", 26 | after_time: "", 27 | after_days: " 天", 28 | completed_before_event: "", 29 | completed_after_event: " 已开始" 30 | }, 31 | search_problem: "搜索题目", 32 | top_users: { 33 | header: "排名", 34 | username: "用户名", 35 | bio: "个性签名", 36 | rating: "积分", 37 | accepted_problem_count: "通过题目数", 38 | no_users: "暂无用户" 39 | }, 40 | friend_links: "友情链接" 41 | }; 42 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/home_settings.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "首页设置", 3 | errors: { 4 | PERMISSION_DENIED: "权限不足。", 5 | NO_SUCH_DISCUSSION: "找不到 ID 为 {id} 的讨论。" 6 | }, 7 | header: "首页设置", 8 | set_default_locale: "设为默认语言", 9 | delete_locale: "删除语言", 10 | confirm_delete_locale: "确认删除语言", 11 | notice: { 12 | header: "通知", 13 | placeholder: "输入通知信息 …" 14 | }, 15 | annnouncements: { 16 | header: "公告", 17 | title: "标题", 18 | date: "日期", 19 | operations: "操作", 20 | add: "添加公告" 21 | }, 22 | hitokoto: "一言", 23 | countdown: "倒计时", 24 | friend_links: "友情链接", 25 | submit: "提交", 26 | success: "修改成功。" 27 | }; 28 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/import.ts: -------------------------------------------------------------------------------- 1 | import importMessages from "../importMessages"; 2 | export default () => importMessages(import.meta.url); 3 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/judge_machine.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "评测机", 3 | header: "评测机", 4 | refresh: "刷新", 5 | add: "添加", 6 | add_new_name: "评测机名称", 7 | confirm_add: "添加", 8 | add_success: "添加成功。", 9 | status: "状态", 10 | name: "名称", 11 | cpu: "CPU", 12 | memory: "内存", 13 | kernel: "内核", 14 | operations: "操作", 15 | online: "在线", 16 | offline: "离线", 17 | key: "密钥", 18 | reset_key: "重置密钥", 19 | confirm_reset_key: "确认重置密钥", 20 | reset_key_success: "密钥已重置。", 21 | delete: "删除", 22 | confirm_delete: "确认删除", 23 | delete_success: "删除成功。", 24 | error: { 25 | PERMISSION_DENIED: "权限不足。", 26 | NO_SUCH_JUDGE_CLIENT: "无此评测机。" 27 | }, 28 | no_judge_machine: "暂无评测机" 29 | }; 30 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/language.js: -------------------------------------------------------------------------------- 1 | return { 2 | zh_CN: "中文(简体)", 3 | en_US: "英语", 4 | ja_JP: "日语" 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/login.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "登录", 3 | login_to_your_account: "登录你的账户", 4 | username_or_email: "用户名 / 邮箱", 5 | password: "密码", 6 | forgot_password: "忘记密码", 7 | remember: "保持登录", 8 | forget: "忘记密码", 9 | new_user: "新用户?", 10 | register: "注册", 11 | login: "登录", 12 | empty_username_or_email: "请输入用户名或邮箱", 13 | invalid_username_or_email: "无效的用户名或邮箱", 14 | empty_password: "请输入密码", 15 | no_such_user: "用户不存在", 16 | wrong_password: "密码错误", 17 | welcome: "欢迎回来,{username}!", 18 | errors: { 19 | NO_SUCH_USER: "无此用户。", 20 | ALREADY_LOGGEDIN: "你已经登陆过。", 21 | ALREADY_MIGRATED: "系统错误,请重试。", 22 | DUPLICATE_USERNAME: "用户名已被使用。" 23 | }, 24 | migration: { 25 | title: "重命名用户", 26 | message: "非常抱歉,您的用户名在新版系统中无效。您必须修改用户名以登录到新系统。", 27 | message_username: "用户名需要仅包含大小写字母、数字和「-_.#$」,且长度在 3 到 24 之间。", 28 | invalid_username: "无效的用户名。", 29 | placeholder: "新用户名", 30 | confirm: "修改用户名", 31 | cancel: "取消" 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/problem_edit.js: -------------------------------------------------------------------------------- 1 | return { 2 | title_edit: "编辑题目", 3 | title_new: "新建题目", 4 | header_edit: "编辑题目 {idString}", 5 | header_new: "新建题目", 6 | back_to_problem: "返回", 7 | confirm_back_to_problem: "放弃修改并返回", 8 | submit: "提交", 9 | no_submit_permission: "无提交权限", 10 | submit_success: "提交成功。", 11 | something_empty: "请填写所有标题与内容。", 12 | error: { 13 | FAILED: "未知错误。", 14 | NO_SUCH_PROBLEM: "无此题目。", 15 | PERMISSION_DENIED: "权限不足。" 16 | }, 17 | header_samples: "样例", 18 | header_tags: "标签", 19 | tags_placeholder: "搜索标签 …", 20 | no_addable_tags: "没有可以添加的标签。", 21 | content_editor: { 22 | title: "标题", 23 | preview_all: "预览", 24 | default: "设为默认语言", 25 | add_default_sections: "添加默认栏目", 26 | confirm_delete: "确认删除语言", 27 | section_title: "栏目标题", 28 | preview: "预览", 29 | add_section: { 30 | before_this_section: "在此栏目前", 31 | after_this_section: "在此栏目后" 32 | }, 33 | section_content: "栏目内容", 34 | new_sample: "新建样例", 35 | sample_input: "样例输入", 36 | sample_output: "样例输出", 37 | sample_explanation: "样例解释", 38 | section_options: { 39 | move_up: "上移", 40 | move_down: "下移", 41 | delete: "删除", 42 | confirm_delete: "确认删除" 43 | }, 44 | section_type: { 45 | text: "文本", 46 | sample: "样例" 47 | } 48 | }, 49 | sample_editor: { 50 | add_sample_when_empty: "添加样例", 51 | sample_id: "样例编号", 52 | warning: { 53 | not_referenced: "未在语言「{language}」中引用过", 54 | multiple_references: "在语言「{language}」中引用了 {referenceCount} 次" 55 | }, 56 | add_sample: { 57 | before_this_sample: "在此样例前", 58 | after_this_sample: "在此样例后" 59 | }, 60 | options: { 61 | move_up: "上移", 62 | move_down: "下移", 63 | delete: "删除", 64 | confirm_delete: "确认删除" 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/problem_files.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "题目文件", 3 | header_testdata: "测试数据", 4 | header_additional_files: "附加文件", 5 | filename: "文件名", 6 | size: "大小", 7 | operations_and_status: "操作 / 状态", 8 | operations: "操作", 9 | delete: "删除", 10 | download_as_archive: "打包下载", 11 | download_as_archive_error: "下载文件 {filename} 时出错:{error}", 12 | confirm_delete: "确认删除", 13 | new_filename: "新文件名", 14 | rename: "重命名", 15 | no_files: "暂无文件", 16 | files_count_and_size: "共 {count} 个文件,总大小 {totalSize}", 17 | files_count_and_size_narrow: "共 {count} 个文件\n总大小 {totalSize}", 18 | files_count_and_size_with_uploading: "共 {count} 个文件,总大小 {totalSize},其中 {uploadingCount} 个正在上传", 19 | files_count_and_size_with_uploading_narrow: "共 {count} 个文件\n总大小 {totalSize}\n其中 {uploadingCount} 个正在上传", 20 | selected_files_count_and_size: "选中 {count} 个文件,总大小 {totalSize}", 21 | selected_files_count_and_size_narrow: "选中 {count} 个文件\n总大小 {totalSize}", 22 | upload: "上传", 23 | confirm_override_question: "确认要覆盖以下文件吗?", 24 | confirm_override: "确认", 25 | progress_waiting: "等待上传", 26 | progress_uploading: "正在上传 {progress}%", 27 | progress_retrying: "重试中", 28 | progress_requesting: "正在请求", 29 | progress_error: "上传出错", 30 | progress_cancelled: "上传取消", 31 | cancel_upload: "取消上传", 32 | back_to_problem: "返回题目", 33 | no_files_to_download: "无任何文件可下载。", 34 | invalid_filename: "无效的文件名。", 35 | error: { 36 | PERMISSION_DENIED: "权限不足。", 37 | NO_SUCH_PROBLEM: "无此题目。", 38 | NO_SUCH_FILE: "无此文件。", 39 | TOO_MANY_FILES: "文件数量超出限制,请联系管理员协助上传。", 40 | TOTAL_SIZE_TOO_LARGE: "数据总大小超出限制,请联系管理员协助上传。", 41 | FILE_UUID_EXISTS: "未知错误。", 42 | FILE_NOT_UPLOADED: "找不到已上传的文件。" 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/problem_set.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "题库", 3 | error: { 4 | PERMISSION_DENIED: "权限不足。", 5 | TAKE_TOO_MANY: "请求每页项目数量超出限制。" 6 | }, 7 | search_tag_placeholder: "标签名 …", 8 | search_icon: { 9 | tag: "标签", 10 | user: "题目拥有者", 11 | nonpublic: "未公开" 12 | }, 13 | search_filters: "搜索条件", 14 | no_matching_tags: "没有匹配的标签", 15 | no_tags: "暂无标签", 16 | show_tags: "显示分类标签", 17 | manage_tags: "管理标签", 18 | add_problem: "添加题目", 19 | no_problem: { 20 | message_search: "找不到符合条件的题目", 21 | message_no_search: "暂无题目", 22 | back: "返回", 23 | clear_filters: "清除搜索条件", 24 | create: "创建题目" 25 | }, 26 | column_status: "状态", 27 | column_title: "题目", 28 | column_submission_count: "提交", 29 | column_accepted_rate: "通过率", 30 | non_public: "未公开", 31 | no_title: "(无标题)" 32 | }; 33 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/problem_tag_manager.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "标签管理", 3 | tag_count: "共 {count} 个标签", 4 | error: { 5 | PERMISSION_DENIED: "权限不足。", 6 | NO_SUCH_PROBLEM_TAG: "无此题目标签。" 7 | }, 8 | no_tags: "暂无标签", 9 | new_tag: "新建标签", 10 | edit_tag: "编辑标签", 11 | name_placeholder: "标签名", 12 | default_language: "默认", 13 | add_language: "添加语言", 14 | confirm_discard_unsaved: "放弃未保存的编辑", 15 | new_tag_button: "新建标签", 16 | submit: "保存", 17 | success_create: "创建成功。", 18 | success_update: "保存成功。", 19 | confirm_delete_language: "确认删除语言", 20 | confirm_delete_tag: "确认删除标签", 21 | close: "关闭" 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/register.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "注册", 3 | register_new_account: "注册新账户", 4 | username: "用户名", 5 | email: "邮箱", 6 | send_email_verification_code: "发送验证码", 7 | email_verification_code_sent: "邮箱验证码已发送。", 8 | email_verification_code: "邮箱验证码", 9 | password: "密码", 10 | retype_password: "确认密码", 11 | register: "注册", 12 | already_have_account: "已有账户?", 13 | login: "登录", 14 | empty_username: "用户名不能为空", 15 | invalid_username: "用户名需要仅包含大小写字母、数字和「-_.#$」,且长度在 3 到 24 之间", 16 | username_already_taken: "用户名已被使用", 17 | empty_email: "邮箱不能为空", 18 | invalid_email: "邮箱无效", 19 | email_already_used: "邮箱已被使用", 20 | invalid_email_verification_code: "邮箱验证码无效", 21 | empty_password: "密码不能为空", 22 | invalid_password: "密码长度需要在 6 到 32 之间", 23 | passwords_do_not_match: "密码不一致", 24 | username_unavailable_message: "用户名不可用。", 25 | email_unavailable_message: "邮箱不可用。", 26 | invalid_password_message: "密码无效。", 27 | passwords_do_not_match_message: "两次输入的密码不一致。", 28 | errors: { 29 | ALREADY_LOGGEDIN: "你已经登陆过。", 30 | DUPLICATE_USERNAME: "用户名已被使用。", 31 | DUPLICATE_EMAIL: "邮箱已被使用。", 32 | INVALID_EMAIL_VERIFICATION_CODE: "邮箱验证码无效。", 33 | FAILED_TO_SEND: "发送邮件失败:{errorMessage}", 34 | RATE_LIMITED: "您的操作过于频繁,请稍后再试。" 35 | }, 36 | success: "注册成功,{username}!" 37 | }; 38 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/submission.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "提交记录", 3 | cancel: "取消", 4 | confirm_cancel: "确认取消", 5 | success_cancel: "成功取消评测。", 6 | rejudge: "重测", 7 | confirm_rejudge: "确认重测", 8 | success_rejudge: "成功重新评测。", 9 | set_public: "设为公开", 10 | confirm_set_public: "确认公开", 11 | success_set_public: "成功公开。", 12 | set_non_public: "取消公开", 13 | confirm_set_non_public: "确认取消公开", 14 | success_set_non_public: "成功取消公开。", 15 | delete: "删除", 16 | confirm_delete: "确认删除", 17 | success_delete: "删除成功。", 18 | error: { 19 | NO_SUCH_PROBLEM: "无此题目。", 20 | NO_SUCH_FILE: "无此文件。", 21 | NO_SUCH_SUBMISSION: "无此提交记录。", 22 | PERMISSION_DENIED: "权限不足。" 23 | }, 24 | failed_to_format: "格式化代码出错:{error}", 25 | format_code: "格式化代码", 26 | show_original_code: "显示原始代码", 27 | compilation_message: "编译信息", 28 | system_message: "系统信息", 29 | sample: "样例", 30 | sample_testcase: "样例", 31 | subtask: { 32 | title: "子任务", 33 | score: "得分:" 34 | }, 35 | testcase: { 36 | title: "测试点", 37 | score: "得分:", 38 | input: "输入文件", 39 | output: "答案文件", 40 | user_output: "你的输出", 41 | user_error: "你的标准错误输出", 42 | checker_message: "检查器信息", 43 | interactor_message: "交互器信息", 44 | system_message: "系统信息" 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/submission_item.js: -------------------------------------------------------------------------------- 1 | return { 2 | columns: { 3 | status: "状态", 4 | score: "分数", 5 | problem: "题目", 6 | time: "用时", 7 | memory: "内存", 8 | answer: "答案", 9 | submitter: "提交者", 10 | submit_time: "提交时间" 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/submission_statistics.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "统计", 3 | error: { 4 | NO_SUCH_PROBLEM: "无此题目。", 5 | PERMISSION_DENIED: "权限不足。", 6 | TAKE_TOO_MANY: "请求每页项目数量超出限制。" 7 | }, 8 | header: "满分提交", 9 | type: { 10 | Fastest: "最快", 11 | MinMemory: "最小内存", 12 | MinAnswerSize: "最短", 13 | Earliest: "最早" 14 | }, 15 | empty: "暂无提交", 16 | empty_goback: "返回", 17 | header_score_distribution: "得分分布", 18 | header_score_prefix_sum: "前缀和", 19 | header_score_suffix_sum: "后缀和", 20 | chart_tooltip: { 21 | score: "分数:", 22 | count: "数量:" 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/submissions.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "提交记录", 3 | query: { 4 | problem_id: "题目", 5 | submitter: "提交者", 6 | code_language: "语言", 7 | code_language_all: "不限语言", 8 | status: "状态", 9 | status_all: "不限状态", 10 | filter: "筛选", 11 | my_submissions: "我的提交" 12 | }, 13 | query_error: { 14 | INVALID_PROBLEM_ID: "无效的题目 ID。", 15 | INVALID_USERNAME: "无效的用户名。", 16 | NO_SUCH_PROBLEM: "无此题目。", 17 | NO_SUCH_USER: "无此用户。" 18 | }, 19 | empty: { 20 | message_filtered: "找不到符合条件的提交", 21 | message_not_filtered: "暂无提交", 22 | goback: "返回" 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/user.js: -------------------------------------------------------------------------------- 1 | return { 2 | error: { 3 | NO_SUCH_USER: "无此用户。" 4 | }, 5 | edit_profile: "修改资料", 6 | joined: "加入于 ", 7 | social: { 8 | email: "电子邮箱", 9 | qq: "QQ", 10 | telegram: "Telegram", 11 | github: "GitHub" 12 | }, 13 | subway_graph: { 14 | start_of_week: "1", 15 | week: { 16 | 1: "周一", 17 | 2: "周二", 18 | 3: "周三", 19 | 4: "周四", 20 | 5: "周五", 21 | 6: "周六", 22 | 7: "周日" 23 | }, 24 | month: { 25 | 1: "一月", 26 | 2: "二月", 27 | 3: "三月", 28 | 4: "四月", 29 | 5: "五月", 30 | 6: "六月", 31 | 7: "七月", 32 | 8: "八月", 33 | 9: "九月", 34 | 10: "十月", 35 | 11: "十一月", 36 | 12: "十二月" 37 | }, 38 | popup: { 39 | submission: "{count} 次提交", 40 | submissions: "{count} 次提交", 41 | no_submissions: "无提交" 42 | }, 43 | link: "查询该用户提交", 44 | legend: { 45 | less: "少", 46 | more: "多" 47 | } 48 | }, 49 | statictics: { 50 | ac_count: "通过题目数", 51 | contest_take_part_count: "参加比赛数", 52 | rating: "积分", 53 | rank: "排名" 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/locales/messages/zh-CN/users.js: -------------------------------------------------------------------------------- 1 | return { 2 | title: "用户", 3 | error: { 4 | TAKE_TOO_MANY: "请求每页项目数量超出限制。" 5 | }, 6 | manage_groups: "管理用户组", 7 | rank: "排名", 8 | username: "用户名", 9 | bio: "个性签名", 10 | accepted_problem_count: "通过题目数", 11 | rating: "积分" 12 | }; 13 | -------------------------------------------------------------------------------- /src/locales/meta.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from "@/interfaces/Locale"; 2 | 3 | export interface LocaleMeta { 4 | name: string; 5 | flag: string; 6 | recaptchaLanguageCode: string; 7 | } 8 | 9 | const localeMeta: Record = { 10 | [Locale.zh_CN]: { 11 | name: "中文(简体)", 12 | flag: "cn", 13 | recaptchaLanguageCode: "zh-CN" 14 | }, 15 | [Locale.en_US]: { 16 | name: "English", 17 | flag: "us", 18 | recaptchaLanguageCode: "en" 19 | }, 20 | [Locale.ja_JP]: { 21 | name: "日本語", 22 | flag: "jp", 23 | recaptchaLanguageCode: "ja" 24 | } 25 | }; 26 | 27 | export default localeMeta; 28 | -------------------------------------------------------------------------------- /src/markdown/MarkdownContent.module.less: -------------------------------------------------------------------------------- 1 | .markdownContent { 2 | position: relative; 3 | overflow: hidden; 4 | transform: translate3d(0, 0, 0); 5 | 6 | // Fix box-shadow get cut 7 | &:not(.noOverflowCutFix) { 8 | padding: 2px; 9 | margin: -2px; 10 | } 11 | 12 | p, blockquote, mjx-container[display="true"] { 13 | overflow: auto; 14 | overflow-y: hidden; 15 | } 16 | 17 | blockquote { 18 | color: var(--theme-blockquote-foreground); 19 | padding: 0 0 0 1em; 20 | border-left: 0.25em solid var(--theme-blockquote-side); 21 | margin: 1em 0; 22 | } 23 | 24 | ul, ol, blockquote, mjx-container[display="true"] { 25 | &:first-child { 26 | margin-top: 0 !important; 27 | } 28 | 29 | &:last-child { 30 | margin-bottom: 0 !important; 31 | } 32 | } 33 | 34 | // Center align images 35 | p > img:only-child { 36 | display: block; 37 | margin: 0 auto; 38 | max-width: 100%; 39 | } 40 | 41 | // Normal lists 42 | ul, ol { 43 | padding-left: 2em; 44 | 45 | ul, ol { 46 | padding-left: 1.5em; 47 | } 48 | } 49 | 50 | // Task lists 51 | :global(.contains-task-list) { 52 | &, ul, ol { 53 | padding-left: 21px; 54 | } 55 | 56 | li { 57 | margin-top: 6px; 58 | 59 | &:global(.task-list-item) { 60 | list-style-type: none; 61 | margin-left: -18.5px; 62 | } 63 | 64 | &:not(:global(.task-list-item)) { 65 | padding-left: 5.5px; 66 | } 67 | } 68 | 69 | .taskListCheckbox > label:first-of-type + div { 70 | padding-left: 24px; 71 | } 72 | } 73 | 74 | mjx-container { 75 | font-size: 1.15em; 76 | 77 | :global(.copy) { 78 | display: inline-block; 79 | width: 0; 80 | height: 0; 81 | font-size: 0; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/markdown/mathjax.ts: -------------------------------------------------------------------------------- 1 | import getScript from "getscript-promise"; 2 | 3 | const TEX_PACKAGES = ["ams", "boldsymbol", "colorv2", "html", "noundefined", "physics"]; 4 | 5 | export const loadMathJax = (() => { 6 | let promise = (async () => { 7 | window.MathJax = { 8 | loader: { 9 | load: ["input/tex-base", ...TEX_PACKAGES.map(packageName => `[tex]/${packageName}`), "output/chtml", "ui/safe"] 10 | }, 11 | tex: { 12 | packages: ["base", ...TEX_PACKAGES] 13 | }, 14 | chtml: { 15 | adaptiveCSS: false 16 | } 17 | }; 18 | await getScript(`${window.cdnjs}/mathjax/${EXTERNAL_PACKAGE_VERSION["mathjax-full"]}/es5/startup.js`); 19 | await window.MathJax.startup.promise; 20 | await window.MathJax.startup.document.updateDocument(); 21 | })(); 22 | promise.then(() => (promise = null)); 23 | 24 | return () => promise; 25 | })(); 26 | 27 | export function renderMath(math: string, display: boolean) { 28 | try { 29 | window.MathJax.texReset(); 30 | const wrapper = window.MathJax.tex2chtml(math, { 31 | display 32 | }) as HTMLElement; 33 | 34 | wrapper.title = math; 35 | 36 | const copy = document.createElement("span"); 37 | copy.className = "copy"; 38 | copy.innerText = math; 39 | wrapper.appendChild(copy); 40 | 41 | return wrapper; 42 | } catch (e) { 43 | console.log(e); 44 | 45 | const wrapper = document.createElement("mjx-container"); 46 | wrapper.className = "MathJax"; 47 | wrapper.setAttribute("jax", "CHTML"); 48 | if (display) wrapper.setAttribute("display", "true"); 49 | 50 | const message = document.createElement("span"); 51 | message.innerText = `Failed to render math, ${String(e)}`; 52 | message.style.fontWeight = "bold"; 53 | message.style.display = "inline-block"; 54 | message.style.border = "2px solid var(--theme-foreground)"; 55 | message.style.padding = "0 4px"; 56 | 57 | wrapper.appendChild(message); 58 | return wrapper; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/markdown/sanitize.ts: -------------------------------------------------------------------------------- 1 | import { FilterXSS, escapeAttrValue } from "xss"; 2 | 3 | // Get the default white list 4 | import { whiteList as xssWhiteList } from "xss/lib/default"; 5 | 6 | // Disallow