diff --git a/.env.example b/.env.example index 66196da8ef6..d2315df9941 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,12 @@ DATA_ENCRYPTION_KEY=data encryption key with 32 char # bundle names like `app://{bundle-name}/` WHITELISTED_ORIGINS=http://localhost:3170,http://localhost:3000,http://localhost:3100,app://localhost_3200,app://hoppscotch +# Optional: Default proxy URL offered to clients when proxying is enabled. +# Clients control whether requests are proxied via the in-app toggle; this only +# sets which URL they default to. Remove the variable to configure it solely +# from the admin dashboard. +PROXY_APP_URL="https://proxy.hoppscotch.io" + # If true, the client’s IP address is understood as the left-most entry in the X-Forwarded-For header TRUST_PROXY=false diff --git a/package.json b/package.json index fbf13590e6e..cc10fda02c5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "Hoppscotch (support@hoppscotch.io)", "private": true, "license": "MIT", - "packageManager": "pnpm@10.33.2", + "packageManager": "pnpm@10.33.4", "scripts": { "preinstall": "npx only-allow pnpm", "prepare": "husky", @@ -35,19 +35,19 @@ }, "pnpm": { "overrides": { - "@nestjs-modules/mailer>mjml": "5.0.0-alpha.4", "@xmldom/xmldom": "0.8.13", "apiconnect-wsdl": "2.0.36", "body-parser": "2.2.1", "cross-spawn": "7.0.6", "execa@<2.0.0": "2.0.0", + "fast-uri@<=3.1.1": "3.1.2", "form-data": "4.0.4", "glob@>=11.0.0 <11.1.0": "11.1.0", "lodash": "4.18.1", "minimatch@>=4.0.0 <4.2.5": "4.2.5", "serialize-javascript@<7.0.3": "7.0.3", "subscriptions-transport-ws>ws": "7.5.10", - "vue": "3.5.33", + "vue": "3.5.34", "ws": "8.17.1" }, "onlyBuiltDependencies": [ diff --git a/packages/hoppscotch-agent/package.json b/packages/hoppscotch-agent/package.json index 37d668e80b0..abbc2fcc2ff 100644 --- a/packages/hoppscotch-agent/package.json +++ b/packages/hoppscotch-agent/package.json @@ -24,7 +24,7 @@ "axios": "1.15.2", "fp-ts": "2.16.11", "lodash-es": "4.18.1", - "vue": "3.5.33" + "vue": "3.5.34" }, "devDependencies": { "@iconify-json/lucide": "1.2.104", diff --git a/packages/hoppscotch-backend/package.json b/packages/hoppscotch-backend/package.json index 2ca42a23d89..8d0f7c1e69a 100644 --- a/packages/hoppscotch-backend/package.json +++ b/packages/hoppscotch-backend/package.json @@ -1,6 +1,6 @@ { "name": "hoppscotch-backend", - "version": "2026.4.1", + "version": "2026.5.0", "description": "", "author": "", "private": true, @@ -31,19 +31,19 @@ "do-test": "pnpm run test" }, "dependencies": { - "@apollo/server": "5.5.0", + "@apollo/server": "5.5.1", "@as-integrations/express5": "1.1.2", - "@nestjs-modules/mailer": "2.3.4", - "@nestjs/apollo": "13.3.0", - "@nestjs/common": "11.1.19", + "@nestjs-modules/mailer": "2.3.5", + "@nestjs/apollo": "13.4.0", + "@nestjs/common": "11.1.21", "@nestjs/config": "4.0.4", - "@nestjs/core": "11.1.19", - "@nestjs/graphql": "13.3.0", + "@nestjs/core": "11.1.21", + "@nestjs/graphql": "13.4.0", "@nestjs/jwt": "11.0.2", "@nestjs/passport": "11.0.0", - "@nestjs/platform-express": "11.1.19", + "@nestjs/platform-express": "11.1.21", "@nestjs/schedule": "6.1.3", - "@nestjs/swagger": "11.4.2", + "@nestjs/swagger": "11.4.3", "@nestjs/terminus": "11.1.1", "@nestjs/throttler": "6.5.0", "@prisma/adapter-pg": "7.8.0", @@ -57,7 +57,7 @@ "dotenv": "17.4.2", "express": "5.2.1", "fp-ts": "2.16.11", - "graphql": "16.13.2", + "graphql": "16.14.0", "graphql-query-complexity": "1.1.0", "graphql-redis-subscriptions": "2.7.0", "graphql-subscriptions": "3.0.0", @@ -71,8 +71,8 @@ "passport-jwt": "4.0.1", "passport-local": "1.0.0", "passport-microsoft": "2.1.0", - "pg": "8.20.0", - "posthog-node": "5.30.6", + "pg": "8.21.0", + "posthog-node": "5.34.6", "prisma": "7.8.0", "reflect-metadata": "0.2.2", "rimraf": "6.1.3", @@ -83,32 +83,32 @@ "@eslint/js": "10.0.1", "@nestjs/cli": "11.0.21", "@nestjs/schematics": "11.1.0", - "@nestjs/testing": "11.1.19", + "@nestjs/testing": "11.1.21", "@relmify/jest-fp-ts": "2.1.1", "@types/bcrypt": "6.0.0", "@types/cookie-parser": "1.4.10", "@types/express": "5.0.6", "@types/jest": "30.0.0", - "@types/node": "25.6.0", + "@types/node": "25.9.0", "@types/nodemailer": "8.0.0", "@types/passport-github2": "1.2.9", "@types/passport-google-oauth20": "2.0.17", "@types/passport-jwt": "4.0.1", "@types/passport-microsoft": "2.1.1", "@types/supertest": "7.2.0", - "@typescript-eslint/eslint-plugin": "8.59.1", - "@typescript-eslint/parser": "8.59.1", + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", "cross-env": "10.1.0", - "eslint": "10.2.1", + "eslint": "10.4.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.5", - "globals": "17.5.0", - "jest": "30.3.0", + "globals": "17.6.0", + "jest": "30.4.2", "jest-mock-extended": "4.0.1", "prettier": "3.8.3", "source-map-support": "0.5.21", "supertest": "7.2.2", - "ts-jest": "29.4.9", + "ts-jest": "29.4.10", "ts-loader": "9.5.7", "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", diff --git a/packages/hoppscotch-backend/src/access-token/dto/create-access-token.dto.ts b/packages/hoppscotch-backend/src/access-token/dto/create-access-token.dto.ts index d837a16a4de..c6b51cb17c7 100644 --- a/packages/hoppscotch-backend/src/access-token/dto/create-access-token.dto.ts +++ b/packages/hoppscotch-backend/src/access-token/dto/create-access-token.dto.ts @@ -1,5 +1,12 @@ +import { IsNotEmpty, IsNumber, IsString, ValidateIf } from 'class-validator'; + // Inputs to create a new PAT export class CreateAccessTokenDto { + @IsString() + @IsNotEmpty() label: string; + + @ValidateIf((o) => o.expiryInDays !== null) + @IsNumber() expiryInDays: number | null; } diff --git a/packages/hoppscotch-backend/src/admin/infra.resolver.ts b/packages/hoppscotch-backend/src/admin/infra.resolver.ts index b822fb1398d..61e6869eb70 100644 --- a/packages/hoppscotch-backend/src/admin/infra.resolver.ts +++ b/packages/hoppscotch-backend/src/admin/infra.resolver.ts @@ -34,6 +34,7 @@ import { } from 'src/infra-config/input-args'; import { InfraConfigEnum } from 'src/types/InfraConfig'; import { ServiceStatus } from 'src/infra-config/helper'; +import { FetchAllTeamsV2Args, FetchAllUsersV2Args } from './input-types.args'; @UseGuards(GqlThrottlerGuard) @Resolver(() => Infra) @@ -92,19 +93,11 @@ export class InfraResolver { description: 'Returns a list of all the users in infra', }) @UseGuards(GqlAuthGuard, GqlAdminGuard) - async allUsersV2( - @Args({ - name: 'searchString', - nullable: true, - description: 'Search on users displayName or email', - }) - searchString: string, - @Args() paginationOption: OffsetPaginationArgs, - ): Promise { - const users = await this.adminService.fetchUsersV2( - searchString, - paginationOption, - ); + async allUsersV2(@Args() args: FetchAllUsersV2Args): Promise { + const users = await this.adminService.fetchUsersV2(args.searchString, { + skip: args.skip, + take: args.take, + }); return users; } @@ -130,19 +123,11 @@ export class InfraResolver { @ResolveField(() => [Team], { description: 'Returns a list of all the teams in the infra', }) - async allTeamsV2( - @Args({ - name: 'searchString', - nullable: true, - description: 'Search on team name or ID', - }) - searchString: string, - @Args() paginationOption: OffsetPaginationArgs, - ): Promise { - const teams = await this.adminService.fetchAllTeamsV2( - searchString, - paginationOption, - ); + async allTeamsV2(@Args() args: FetchAllTeamsV2Args): Promise { + const teams = await this.adminService.fetchAllTeamsV2(args.searchString, { + skip: args.skip, + take: args.take, + }); return teams; } diff --git a/packages/hoppscotch-backend/src/admin/input-types.args.ts b/packages/hoppscotch-backend/src/admin/input-types.args.ts index 129a1357ef6..d7f1258395f 100644 --- a/packages/hoppscotch-backend/src/admin/input-types.args.ts +++ b/packages/hoppscotch-backend/src/admin/input-types.args.ts @@ -1,19 +1,51 @@ import { Field, ID, ArgsType } from '@nestjs/graphql'; import { TeamAccessRole } from '../team/team.model'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { OffsetPaginationArgs } from 'src/types/input-types.args'; + +@ArgsType() +export class FetchAllUsersV2Args extends OffsetPaginationArgs { + @Field({ + name: 'searchString', + nullable: true, + description: 'Search on users displayName or email', + }) + @IsString() + @IsOptional() + searchString: string; +} + +@ArgsType() +export class FetchAllTeamsV2Args extends OffsetPaginationArgs { + @Field({ + name: 'searchString', + nullable: true, + description: 'Search on team name or ID', + }) + @IsString() + @IsOptional() + searchString: string; +} @ArgsType() export class ChangeUserRoleInTeamArgs { + @IsString() + @IsNotEmpty() @Field(() => ID, { name: 'userUID', description: 'users UID', }) userUID: string; + + @IsString() + @IsNotEmpty() @Field(() => ID, { name: 'teamID', description: 'team ID', }) teamID: string; + @IsEnum(TeamAccessRole) @Field(() => TeamAccessRole, { name: 'newRole', description: 'updated team role', @@ -23,18 +55,23 @@ export class ChangeUserRoleInTeamArgs { @ArgsType() export class AddUserToTeamArgs { + @IsString() + @IsNotEmpty() @Field(() => ID, { name: 'teamID', description: 'team ID', }) teamID: string; + @IsEnum(TeamAccessRole) @Field(() => TeamAccessRole, { name: 'role', description: 'The role of the user to add in the team', }) role: TeamAccessRole; + @IsString() + @IsNotEmpty() @Field({ name: 'userEmail', description: 'Email of the user to add to team', diff --git a/packages/hoppscotch-backend/src/auth/dto/signin-magic.dto.ts b/packages/hoppscotch-backend/src/auth/dto/signin-magic.dto.ts index a3b1aa27ca4..e6ddc1469fa 100644 --- a/packages/hoppscotch-backend/src/auth/dto/signin-magic.dto.ts +++ b/packages/hoppscotch-backend/src/auth/dto/signin-magic.dto.ts @@ -1,4 +1,7 @@ +import { IsEmail } from 'class-validator'; + // Inputs to initiate Magic-Link auth flow export class SignInMagicDto { + @IsEmail() email: string; } diff --git a/packages/hoppscotch-backend/src/auth/dto/verify-magic.dto.ts b/packages/hoppscotch-backend/src/auth/dto/verify-magic.dto.ts index 958729dd480..27f9a439a87 100644 --- a/packages/hoppscotch-backend/src/auth/dto/verify-magic.dto.ts +++ b/packages/hoppscotch-backend/src/auth/dto/verify-magic.dto.ts @@ -1,5 +1,12 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + // Inputs to verify and sign a user in via magic-link export class VerifyMagicDto { + @IsString() + @IsNotEmpty() deviceIdentifier: string; + + @IsString() + @IsNotEmpty() token: string; } diff --git a/packages/hoppscotch-backend/src/infra-config/dto/onboarding.dto.ts b/packages/hoppscotch-backend/src/infra-config/dto/onboarding.dto.ts index c3e38ec7ebd..9397e3819d4 100644 --- a/packages/hoppscotch-backend/src/infra-config/dto/onboarding.dto.ts +++ b/packages/hoppscotch-backend/src/infra-config/dto/onboarding.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; -import { IsOptional, IsString } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { InfraConfigEnum } from 'src/types/InfraConfig'; export class GetOnboardingStatusResponse { @@ -15,6 +15,7 @@ export class GetOnboardingStatusResponse { export class SaveOnboardingConfigRequest { @ApiProperty() @IsString() + @IsNotEmpty() [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS]: string; @ApiPropertyOptional() @@ -84,6 +85,7 @@ export class SaveOnboardingConfigRequest { @ApiPropertyOptional() @IsOptional() + @IsString() [InfraConfigEnum.MAILER_SMTP_URL]: string; @ApiPropertyOptional() @@ -137,7 +139,6 @@ export class SaveOnboardingConfigRequest { [InfraConfigEnum.MAILER_SMTP_OAUTH2_REFRESH_TOKEN]: string; @ApiPropertyOptional() @IsOptional() - @IsString() [InfraConfigEnum.MAILER_SMTP_OAUTH2_ACCESS_URL]: string; } diff --git a/packages/hoppscotch-backend/src/infra-config/helper.ts b/packages/hoppscotch-backend/src/infra-config/helper.ts index b2171817a5f..d00537362cd 100644 --- a/packages/hoppscotch-backend/src/infra-config/helper.ts +++ b/packages/hoppscotch-backend/src/infra-config/helper.ts @@ -5,12 +5,15 @@ import { InfraConfigEnum } from 'src/types/InfraConfig'; import { SMTPAuthType } from 'src/mailer/helper'; import { decrypt, encrypt } from 'src/utils'; import { randomBytes } from 'crypto'; +import { InfraConfig } from 'src/generated/prisma/client'; export enum ServiceStatus { ENABLE = 'ENABLE', DISABLE = 'DISABLE', } +const SYNC_ONLY_VARIABLES = [InfraConfigEnum.PROXY_APP_URL]; + type DefaultInfraConfig = { name: InfraConfigEnum; value: string; @@ -344,6 +347,11 @@ export async function getDefaultInfraConfigs(): Promise { value: null, isEncrypted: false, }, + { + name: InfraConfigEnum.PROXY_APP_URL, + value: process.env.PROXY_APP_URL || 'https://proxy.hoppscotch.io', + isEncrypted: false, + }, { name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION, value: false.toString(), @@ -413,6 +421,54 @@ export async function getEncryptionRequiredInfraConfigEntries( return requiredEncryption; } +/** + * Sync the 'infra_config' table with .env file + * @returns Array of InfraConfig + */ +export async function syncInfraConfigWithEnvFile() { + const prisma = getSharedPrismaInstance(); + const dbInfraConfigs = await prisma.infraConfig.findMany({ + where: { name: { in: SYNC_ONLY_VARIABLES } }, + }); + + const updateRequiredObjs: (Partial & { id: string })[] = []; + + for (const dbConfig of dbInfraConfigs) { + const envValue = process.env[dbConfig.name]; + + // If the env var is unset, leave the admin-set DB value alone. Otherwise + // an admin's later override would be wiped on every restart. + if (envValue === undefined) continue; + + // lastSyncedEnvFileValue null check for backward compatibility from 2024.10.2 and below + if (!dbConfig.lastSyncedEnvFileValue) { + const configValue = dbConfig.isEncrypted ? encrypt(envValue) : envValue; + updateRequiredObjs.push({ + id: dbConfig.id, + value: dbConfig.value === null ? configValue : undefined, + lastSyncedEnvFileValue: configValue, + }); + continue; + } + + // If the value in the database is different from the value in the .env file, means the value in the .env file has been updated + const rawLastSyncedEnvFileValue = dbConfig.isEncrypted + ? decrypt(dbConfig.lastSyncedEnvFileValue) + : dbConfig.lastSyncedEnvFileValue; + + if (rawLastSyncedEnvFileValue !== envValue) { + const configValue = dbConfig.isEncrypted ? encrypt(envValue) : envValue; + updateRequiredObjs.push({ + id: dbConfig.id, + value: configValue, + lastSyncedEnvFileValue: configValue, + }); + } + } + + return updateRequiredObjs; +} + /** * Verify if 'infra_config' table is loaded with all entries * @returns boolean diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.resolver.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.resolver.ts index aeb08cd3e42..1da74f8b64f 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.resolver.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.resolver.ts @@ -38,6 +38,18 @@ export class InfraConfigResolver { return isEnabled.right; } + @Query(() => InfraConfig, { + description: 'Get the proxy app URL', + }) + // Public by design: the unauthenticated selfhost client needs this URL before login. + async proxyAppUrl() { + const infraConfig = await this.infraConfigService.get( + InfraConfigEnum.PROXY_APP_URL, + ); + if (E.isLeft(infraConfig)) throwErr(infraConfig.left); + return infraConfig.right; + } + /* Subscriptions */ @Subscription(() => String, { diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts index bac795407b1..99f782a1a53 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts @@ -274,6 +274,67 @@ describe('InfraConfigService', () => { }); }); + describe('updateOnboardingConfig (allowlist filtering)', () => { + it('should drop keys outside ONBOARDING_ALLOWED_KEYS before persisting', async () => { + // Pretend the DTO has extra disallowed keys (mimicking a bypass of the + // ValidationPipe, e.g. an internal caller). The service must still not + // persist keys like JWT_SECRET / SESSION_SECRET / ALLOW_SECURE_COOKIES. + const dto = { + [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS]: 'GOOGLE', + [InfraConfigEnum.GOOGLE_CLIENT_ID]: 'gid', + [InfraConfigEnum.GOOGLE_CLIENT_SECRET]: 'gsecret', + [InfraConfigEnum.GOOGLE_CALLBACK_URL]: 'https://example.com/cb', + [InfraConfigEnum.GOOGLE_SCOPE]: 'email', + [InfraConfigEnum.JWT_SECRET]: 'ATTACKER', + [InfraConfigEnum.SESSION_SECRET]: 'ATTACKER', + [InfraConfigEnum.ALLOW_SECURE_COOKIES]: 'true', + } as any; + + const updateManySpy = jest + .spyOn(infraConfigService, 'updateMany') + .mockResolvedValueOnce(E.right([] as any)); + + await infraConfigService.updateOnboardingConfig(dto); + + expect(updateManySpy).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, + value: 'GOOGLE', + }), + expect.objectContaining({ + name: InfraConfigEnum.GOOGLE_CLIENT_ID, + value: 'gid', + }), + expect.objectContaining({ + name: InfraConfigEnum.GOOGLE_CLIENT_SECRET, + value: 'gsecret', + }), + expect.objectContaining({ + name: InfraConfigEnum.GOOGLE_CALLBACK_URL, + value: 'https://example.com/cb', + }), + expect.objectContaining({ + name: InfraConfigEnum.GOOGLE_SCOPE, + value: 'email', + }), + + expect.objectContaining({ + name: InfraConfigEnum.ONBOARDING_COMPLETED, + value: 'true', + }), + expect.objectContaining({ + name: InfraConfigEnum.ONBOARDING_RECOVERY_TOKEN, + value: expect.any(String), + }), + ]), + false, + ); + + updateManySpy.mockRestore(); + }); + }); + describe('isUserHistoryEnabled', () => { it('should return true if the user history is enabled', async () => { const response = { diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts index 601d5ebbe69..15b90a4925b 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts @@ -33,6 +33,7 @@ import { getEncryptionRequiredInfraConfigEntries, getMissingInfraConfigEntries, stopApp, + syncInfraConfigWithEnvFile, } from './helper'; import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args'; import { AuthProvider } from 'src/auth/helper'; @@ -123,11 +124,30 @@ export class InfraConfigService implements OnModuleInit, OnModuleDestroy { await Promise.allSettled(dbOperations); } - // Restart the app if needed + // Sync the InfraConfigs with the .env file, if .env file updates later on + const envFileChangesRequired = await syncInfraConfigWithEnvFile(); + if (envFileChangesRequired.length > 0) { + const dbOperations = envFileChangesRequired.map((dbConfig) => { + const { id, ...dataObj } = dbConfig; + return this.prisma.infraConfig.update({ + where: { id: dbConfig.id }, + data: dataObj, + }); + }); + await Promise.allSettled(dbOperations); + } + + // Restart the app if needed. Metadata-only sync writes (where `value` + // is undefined because only `lastSyncedEnvFileValue` is being persisted) + // don't change runtime config, so they shouldn't trigger a restart. + const envValueChanged = envFileChangesRequired.some( + (c) => c.value !== undefined, + ); if ( propsToInsert.length > 0 || encryptionRequiredEntries.length > 0 || - Object.keys(derivedEnv).length > 0 + Object.keys(derivedEnv).length > 0 || + envValueChanged ) { stopApp(); } @@ -538,7 +558,11 @@ export class InfraConfigService implements OnModuleInit, OnModuleDestroy { const configEntries: InfraConfigArgs[] = [ ...Object.entries(dto) - .filter(([_, value]) => value !== undefined) + .filter( + ([key, value]) => + value !== undefined && + Object.keys(new SaveOnboardingConfigRequest()).includes(key), + ) .map(([key, value]) => ({ name: key as InfraConfigEnum, value, @@ -790,6 +814,7 @@ export class InfraConfigService implements OnModuleInit, OnModuleDestroy { case InfraConfigEnum.GOOGLE_CALLBACK_URL: case InfraConfigEnum.GITHUB_CALLBACK_URL: case InfraConfigEnum.MICROSOFT_CALLBACK_URL: + case InfraConfigEnum.PROXY_APP_URL: if (!validateUrl(value)) return fail(); break; diff --git a/packages/hoppscotch-backend/src/infra-config/input-args.ts b/packages/hoppscotch-backend/src/infra-config/input-args.ts index fd248e52634..0f09418608a 100644 --- a/packages/hoppscotch-backend/src/infra-config/input-args.ts +++ b/packages/hoppscotch-backend/src/infra-config/input-args.ts @@ -2,14 +2,17 @@ import { Field, InputType } from '@nestjs/graphql'; import { InfraConfigEnum } from 'src/types/InfraConfig'; import { ServiceStatus } from './helper'; import { AuthProvider } from 'src/auth/helper'; +import { IsEnum, IsString } from 'class-validator'; @InputType() export class InfraConfigArgs { + @IsEnum(InfraConfigEnum) @Field(() => InfraConfigEnum, { description: 'Infra Config Name', }) name: InfraConfigEnum; + @IsString() @Field({ description: 'Infra Config Value', }) @@ -18,11 +21,13 @@ export class InfraConfigArgs { @InputType() export class EnableAndDisableSSOArgs { + @IsEnum(AuthProvider) @Field(() => AuthProvider, { description: 'Auth Provider', }) provider: AuthProvider; + @IsEnum(ServiceStatus) @Field(() => ServiceStatus, { description: 'Auth Provider Status', }) diff --git a/packages/hoppscotch-backend/src/infra-token/request-response.dto.ts b/packages/hoppscotch-backend/src/infra-token/request-response.dto.ts index 99313e49e67..81493346216 100644 --- a/packages/hoppscotch-backend/src/infra-token/request-response.dto.ts +++ b/packages/hoppscotch-backend/src/infra-token/request-response.dto.ts @@ -4,7 +4,7 @@ import { ArrayMinSize, IsArray, IsBoolean, - IsNotEmpty, + IsEmail, IsOptional, IsString, MinLength, @@ -14,9 +14,9 @@ import { OffsetPaginationArgs } from 'src/types/input-types.args'; // POST v1/infra/user-invitations export class CreateUserInvitationRequest { - @Type(() => String) - @IsNotEmpty() + @IsEmail() @ApiProperty() + @Type(() => String) inviteeEmail: string; } export class CreateUserInvitationResponse { @@ -40,8 +40,8 @@ export class GetUserInvitationResponse { export class DeleteUserInvitationRequest { @IsArray() @ArrayMinSize(1) + @IsEmail({}, { each: true }) @Type(() => String) - @IsNotEmpty() @ApiProperty() inviteeEmails: string[]; } @@ -53,8 +53,8 @@ export class DeleteUserInvitationResponse { // POST v1/infra/users export class GetUsersRequestQuery extends OffsetPaginationArgs { - @IsOptional() @IsString() + @IsOptional() @MinLength(1) @ApiPropertyOptional() searchString: string; @@ -101,7 +101,6 @@ export class UpdateUserRequest { // PATCH v1/infra/users/:uid/admin-status export class UpdateUserAdminStatusRequest { @IsBoolean() - @IsNotEmpty() @ApiProperty() isAdmin: boolean; } diff --git a/packages/hoppscotch-backend/src/main.ts b/packages/hoppscotch-backend/src/main.ts index 94be62be400..0244015e7ae 100644 --- a/packages/hoppscotch-backend/src/main.ts +++ b/packages/hoppscotch-backend/src/main.ts @@ -76,6 +76,8 @@ async function bootstrap() { app.useGlobalPipes( new ValidationPipe({ transform: true, + whitelist: true, + forbidNonWhitelisted: true, }), ); diff --git a/packages/hoppscotch-backend/src/mock-server/mock-server.model.ts b/packages/hoppscotch-backend/src/mock-server/mock-server.model.ts index 69c5beb1818..e53a76aa4dc 100644 --- a/packages/hoppscotch-backend/src/mock-server/mock-server.model.ts +++ b/packages/hoppscotch-backend/src/mock-server/mock-server.model.ts @@ -7,14 +7,19 @@ import { registerEnumType, } from '@nestjs/graphql'; import { + IsBoolean, + IsEnum, + IsNotEmpty, IsNumber, IsOptional, IsString, Matches, Max, MaxLength, + Min, MinLength, } from 'class-validator'; +import { OffsetPaginationArgs } from 'src/types/input-types.args'; import { WorkspaceType } from 'src/types/WorkspaceTypes'; // Regex pattern for mock server name validation @@ -117,6 +122,8 @@ export class CreateMockServerInput { }) name: string; + @IsString() + @IsOptional() @Field({ nullable: true, description: @@ -124,6 +131,8 @@ export class CreateMockServerInput { }) collectionID?: string; + @IsBoolean() + @IsOptional() @Field({ nullable: true, description: @@ -131,6 +140,8 @@ export class CreateMockServerInput { }) autoCreateCollection?: boolean; + @IsBoolean() + @IsOptional() @Field({ nullable: true, description: @@ -138,11 +149,14 @@ export class CreateMockServerInput { }) autoCreateRequestExample?: boolean; + @IsEnum(WorkspaceType) @Field(() => WorkspaceType, { description: 'Type of workspace: USER or TEAM', }) workspaceType: WorkspaceType; + @IsOptional() + @IsString() @Field({ nullable: true, description: @@ -150,16 +164,19 @@ export class CreateMockServerInput { }) workspaceID?: string; + @IsNumber() + @IsOptional() + @Min(0) + @Max(60000) @Field({ nullable: true, defaultValue: 0, description: 'Delay in milliseconds before responding', }) - @IsOptional() - @IsNumber() - @Max(60000) delayInMs?: number; + @IsBoolean() + @IsOptional() @Field({ nullable: true, defaultValue: true, @@ -189,6 +206,7 @@ export class UpdateMockServerInput { }) @IsOptional() @IsNumber() + @Min(0) @Max(60000) delayInMs?: number; @@ -196,12 +214,16 @@ export class UpdateMockServerInput { nullable: true, description: 'Whether the mock server is active', }) + @IsOptional() + @IsBoolean() isActive?: boolean; @Field({ nullable: true, description: 'Whether the mock server is publicly accessible', }) + @IsOptional() + @IsBoolean() isPublic?: boolean; } @@ -236,9 +258,22 @@ export class MockServerMutationArgs { @Field(() => ID, { description: 'ID of the mock server', }) + @IsString() + @IsNotEmpty() id: string; } +@ArgsType() +export class FetchTeamMockServersArgs extends OffsetPaginationArgs { + @Field(() => ID, { + name: 'teamID', + description: 'Id of the team to add to', + }) + @IsString() + @IsNotEmpty() + teamID: string; +} + @ObjectType() export class MockServerLog { @Field(() => ID, { diff --git a/packages/hoppscotch-backend/src/mock-server/mock-server.resolver.ts b/packages/hoppscotch-backend/src/mock-server/mock-server.resolver.ts index 64e2595c6dc..423e814e6c6 100644 --- a/packages/hoppscotch-backend/src/mock-server/mock-server.resolver.ts +++ b/packages/hoppscotch-backend/src/mock-server/mock-server.resolver.ts @@ -19,6 +19,7 @@ import { MockServerMutationArgs, MockServerCollection, MockServerLog, + FetchTeamMockServersArgs, } from './mock-server.model'; import * as E from 'fp-ts/Either'; import { OffsetPaginationArgs } from 'src/types/input-types.args'; @@ -90,15 +91,12 @@ export class MockServerResolver { TeamAccessRole.OWNER, ) async teamMockServers( - @Args({ - name: 'teamID', - type: () => ID, - description: 'Id of the team to add to', - }) - teamID: string, - @Args() args: OffsetPaginationArgs, + @Args() args: FetchTeamMockServersArgs, ): Promise { - return this.mockServerService.getTeamMockServers(teamID, args); + return this.mockServerService.getTeamMockServers(args.teamID, { + skip: args.skip, + take: args.take, + }); } @Query(() => MockServer, { diff --git a/packages/hoppscotch-backend/src/published-docs/input-type.args.ts b/packages/hoppscotch-backend/src/published-docs/input-type.args.ts index af3b9681b44..f0bd316e379 100644 --- a/packages/hoppscotch-backend/src/published-docs/input-type.args.ts +++ b/packages/hoppscotch-backend/src/published-docs/input-type.args.ts @@ -1,15 +1,46 @@ -import { InputType, Field } from '@nestjs/graphql'; -import { IsOptional, Matches } from 'class-validator'; +import { InputType, Field, ArgsType, ID } from '@nestjs/graphql'; +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + Matches, +} from 'class-validator'; +import { OffsetPaginationArgs } from 'src/types/input-types.args'; import { WorkspaceType } from 'src/types/WorkspaceTypes'; +@ArgsType() +export class FetchPublishedDocsArgs extends OffsetPaginationArgs { + @IsNotEmpty() + @Field(() => ID, { + name: 'teamID', + description: 'ID of the team', + }) + teamID: string; + + @Field(() => ID, { + name: 'collectionID', + description: 'Id of the collection to add to', + nullable: true, + }) + @IsString() + @IsOptional() + collectionID: string | undefined; +} + @InputType() export class CreatePublishedDocsArgs { + @IsString() + @IsNotEmpty() @Field({ name: 'title', description: 'Title of the published document', }) title: string; + @IsString() + @IsNotEmpty() @Field({ name: 'version', description: 'Version of the published document', @@ -20,6 +51,7 @@ export class CreatePublishedDocsArgs { }) version: string; + @IsBoolean() @Field({ name: 'autoSync', description: @@ -27,18 +59,22 @@ export class CreatePublishedDocsArgs { }) autoSync: boolean; + @IsEnum(WorkspaceType) @Field(() => WorkspaceType, { name: 'workspaceType', description: 'Type of the workspace (e.g., personal, team)', }) workspaceType: WorkspaceType; + @IsString() @Field({ name: 'workspaceID', description: 'ID of the workspace', }) workspaceID: string; + @IsString() + @IsNotEmpty() @Field({ name: 'collectionID', description: @@ -46,6 +82,8 @@ export class CreatePublishedDocsArgs { }) collectionID: string; + @IsString() + @IsNotEmpty() @Field({ name: 'metadata', description: 'Metadata associated with the published document', @@ -59,6 +97,7 @@ export class CreatePublishedDocsArgs { nullable: true, }) @IsOptional() + @IsString() environmentID?: string; } @@ -69,6 +108,7 @@ export class UpdatePublishedDocsArgs { description: 'Title of the published document', nullable: true, }) + @IsString() @IsOptional() title?: string; @@ -77,6 +117,7 @@ export class UpdatePublishedDocsArgs { description: 'Version of the published document', nullable: true, }) + @IsString() @IsOptional() @Matches(/^[a-zA-Z0-9.-]+$/, { message: @@ -90,6 +131,7 @@ export class UpdatePublishedDocsArgs { 'Whether the published document should auto-sync with the source', nullable: true, }) + @IsBoolean() @IsOptional() autoSync?: boolean; @@ -98,6 +140,7 @@ export class UpdatePublishedDocsArgs { description: 'Metadata associated with the published document', nullable: true, }) + @IsString() @IsOptional() metadata?: string; @@ -107,6 +150,7 @@ export class UpdatePublishedDocsArgs { 'ID of the environment to associate with the published document. Pass null to remove the environment.', nullable: true, }) + @IsString() @IsOptional() environmentID?: string; } diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.resolver.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.resolver.ts index ecb263236f7..4a4d3f967a8 100644 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.resolver.ts +++ b/packages/hoppscotch-backend/src/published-docs/published-docs.resolver.ts @@ -18,6 +18,7 @@ import { GqlAuthGuard } from 'src/guards/gql-auth.guard'; import { GqlUser } from 'src/decorators/gql-user.decorator'; import { CreatePublishedDocsArgs, + FetchPublishedDocsArgs, UpdatePublishedDocsArgs, } from './input-type.args'; import { User } from 'src/user/user.model'; @@ -124,25 +125,13 @@ export class PublishedDocsResolver { TeamAccessRole.OWNER, ) async teamPublishedDocsList( - @Args({ - name: 'teamID', - type: () => ID, - description: 'Id of the team to add to', - }) - teamID: string, - @Args({ - name: 'collectionID', - type: () => ID, - description: 'Id of the collection to add to', - nullable: true, - }) - collectionID: string | undefined, - @Args() args: OffsetPaginationArgs, + @Args() + args: FetchPublishedDocsArgs, ) { const docs = await this.publishedDocsService.getAllTeamPublishedDocs( - teamID, - collectionID, - args, + args.teamID, + args.collectionID, + { skip: args.skip, take: args.take }, ); return docs; } diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts index c3b3aa0bbc3..1b1680235fb 100644 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts +++ b/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts @@ -4,7 +4,6 @@ import { PUBLISHED_DOCS_CREATION_FAILED, PUBLISHED_DOCS_DELETION_FAILED, PUBLISHED_DOCS_INVALID_COLLECTION, - PUBLISHED_DOCS_FORBIDDEN_ENVIRONMENT_ACCESS, PUBLISHED_DOCS_NOT_FOUND, PUBLISHED_DOCS_UPDATE_FAILED, TEAM_ENVIRONMENT_NOT_FOUND, diff --git a/packages/hoppscotch-backend/src/team-collection/input-type.args.ts b/packages/hoppscotch-backend/src/team-collection/input-type.args.ts index 24e70c5046b..5a87f7ed0f8 100644 --- a/packages/hoppscotch-backend/src/team-collection/input-type.args.ts +++ b/packages/hoppscotch-backend/src/team-collection/input-type.args.ts @@ -1,18 +1,25 @@ import { ArgsType, Field, ID } from '@nestjs/graphql'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { PaginationArgs } from 'src/types/input-types.args'; @ArgsType() export class GetRootTeamCollectionsArgs extends PaginationArgs { @Field(() => ID, { name: 'teamID', description: 'ID of the team' }) + @IsString() + @IsNotEmpty() teamID: string; } @ArgsType() export class CreateRootTeamCollectionArgs { @Field(() => ID, { name: 'teamID', description: 'ID of the team' }) + @IsString() + @IsNotEmpty() teamID: string; @Field({ name: 'title', description: 'Title of the new collection' }) + @IsString() + @IsNotEmpty() title: string; @Field({ @@ -20,6 +27,8 @@ export class CreateRootTeamCollectionArgs { description: 'JSON string representing the collection data', nullable: true, }) + @IsString() + @IsOptional() data: string; } @@ -29,9 +38,13 @@ export class CreateChildTeamCollectionArgs { name: 'collectionID', description: 'ID of the parent to the new collection', }) + @IsString() + @IsNotEmpty() collectionID: string; @Field({ name: 'childTitle', description: 'Title of the new collection' }) + @IsString() + @IsNotEmpty() childTitle: string; @Field({ @@ -39,6 +52,8 @@ export class CreateChildTeamCollectionArgs { description: 'JSON string representing the collection data', nullable: true, }) + @IsString() + @IsOptional() data: string; } @@ -48,12 +63,16 @@ export class RenameTeamCollectionArgs { name: 'collectionID', description: 'ID of the collection', }) + @IsString() + @IsNotEmpty() collectionID: string; @Field({ name: 'newTitle', description: 'The updated title of the collection', }) + @IsString() + @IsNotEmpty() newTitle: string; } @@ -64,12 +83,16 @@ export class MoveTeamCollectionArgs { description: 'ID of the parent to the new collection', nullable: true, }) + @IsString() + @IsOptional() parentCollectionID: string; @Field(() => ID, { name: 'collectionID', description: 'ID of the collection', }) + @IsString() + @IsNotEmpty() collectionID: string; } @@ -79,6 +102,8 @@ export class UpdateTeamCollectionOrderArgs { name: 'collectionID', description: 'ID of the collection', }) + @IsString() + @IsNotEmpty() collectionID: string; @Field(() => ID, { @@ -87,6 +112,8 @@ export class UpdateTeamCollectionOrderArgs { 'ID of the collection that comes after the updated collection in its new position', nullable: true, }) + @IsString() + @IsOptional() destCollID: string; } @@ -96,6 +123,8 @@ export class UpdateTeamCollectionArgs { name: 'collectionID', description: 'ID of the collection', }) + @IsString() + @IsNotEmpty() collectionID: string; @Field({ @@ -103,6 +132,8 @@ export class UpdateTeamCollectionArgs { description: 'The updated title of the collection', nullable: true, }) + @IsString() + @IsOptional() newTitle: string; @Field({ @@ -110,5 +141,7 @@ export class UpdateTeamCollectionArgs { description: 'JSON string representing the collection data', nullable: true, }) + @IsString() + @IsOptional() data: string; } diff --git a/packages/hoppscotch-backend/src/team-environments/input-type.args.ts b/packages/hoppscotch-backend/src/team-environments/input-type.args.ts index 393865fabc1..a58d9af17c4 100644 --- a/packages/hoppscotch-backend/src/team-environments/input-type.args.ts +++ b/packages/hoppscotch-backend/src/team-environments/input-type.args.ts @@ -1,4 +1,5 @@ import { ArgsType, Field, ID } from '@nestjs/graphql'; +import { IsString, IsNotEmpty } from 'class-validator'; @ArgsType() export class CreateTeamEnvironmentArgs { @@ -6,18 +7,24 @@ export class CreateTeamEnvironmentArgs { name: 'name', description: 'Name of the Team Environment', }) + @IsString() + @IsNotEmpty() name: string; @Field(() => ID, { name: 'teamID', description: 'ID of the Team', }) + @IsString() + @IsNotEmpty() teamID: string; @Field({ name: 'variables', description: 'JSON string of the variables object', }) + @IsString() + @IsNotEmpty() variables: string; } @@ -27,15 +34,23 @@ export class UpdateTeamEnvironmentArgs { name: 'id', description: 'ID of the Team Environment', }) + @IsString() + @IsNotEmpty() id: string; + @Field({ name: 'name', description: 'Name of the Team Environment', }) + @IsString() + @IsNotEmpty() name: string; + @Field({ name: 'variables', description: 'JSON string of the variables object', }) + @IsString() + @IsNotEmpty() variables: string; } diff --git a/packages/hoppscotch-backend/src/team-invitation/input-type.args.ts b/packages/hoppscotch-backend/src/team-invitation/input-type.args.ts index 3bbaedc9e8f..5d3c23c5ea3 100644 --- a/packages/hoppscotch-backend/src/team-invitation/input-type.args.ts +++ b/packages/hoppscotch-backend/src/team-invitation/input-type.args.ts @@ -1,4 +1,5 @@ import { ArgsType, Field, ID } from '@nestjs/graphql'; +import { IsEmail, IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { TeamAccessRole } from 'src/team/team.model'; @ArgsType() @@ -7,14 +8,18 @@ export class CreateTeamInvitationArgs { name: 'teamID', description: 'ID of the Team ID to invite from', }) + @IsString() + @IsNotEmpty() teamID: string; @Field({ name: 'inviteeEmail', description: 'Email of the user to invite' }) + @IsEmail() inviteeEmail: string; @Field(() => TeamAccessRole, { name: 'inviteeRole', description: 'Role to be given to the user', }) + @IsEnum(TeamAccessRole) inviteeRole: TeamAccessRole; } diff --git a/packages/hoppscotch-backend/src/team-request/input-type.args.ts b/packages/hoppscotch-backend/src/team-request/input-type.args.ts index 5c88fd5d964..d79ec790b5f 100644 --- a/packages/hoppscotch-backend/src/team-request/input-type.args.ts +++ b/packages/hoppscotch-backend/src/team-request/input-type.args.ts @@ -1,4 +1,5 @@ import { Field, ID, InputType, ArgsType } from '@nestjs/graphql'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { PaginationArgs } from 'src/types/input-types.args'; @InputType() @@ -6,16 +7,22 @@ export class CreateTeamRequestInput { @Field(() => ID, { description: 'ID of the team the collection belongs to', }) + @IsString() + @IsNotEmpty() teamID: string; @Field({ description: 'JSON string representing the request data', }) + @IsString() + @IsNotEmpty() request: string; @Field({ description: 'Displayed title of the request', }) + @IsString() + @IsNotEmpty() title: string; } @@ -25,12 +32,16 @@ export class UpdateTeamRequestInput { description: 'JSON string representing the request data', nullable: true, }) + @IsString() + @IsOptional() request?: string; @Field({ description: 'Displayed title of the request', nullable: true, }) + @IsString() + @IsOptional() title?: string; } @@ -39,11 +50,15 @@ export class SearchTeamRequestArgs extends PaginationArgs { @Field(() => ID, { description: 'ID of the team to look in', }) + @IsString() + @IsNotEmpty() teamID: string; @Field({ description: 'The title to search for', }) + @IsString() + @IsNotEmpty() searchTerm: string; } @@ -55,16 +70,22 @@ export class MoveTeamRequestArgs { defaultValue: undefined, description: 'ID of the collection, the request belong to', }) + @IsString() + @IsOptional() srcCollID: string; @Field(() => ID, { description: 'ID of the request to move', }) + @IsString() + @IsNotEmpty() requestID: string; @Field(() => ID, { description: 'ID of the collection, where the request is moving to', }) + @IsString() + @IsNotEmpty() destCollID: string; @Field(() => ID, { @@ -72,6 +93,8 @@ export class MoveTeamRequestArgs { description: 'ID of the request that comes after the updated request in its new position', }) + @IsString() + @IsOptional() nextRequestID: string; } @@ -80,6 +103,8 @@ export class UpdateLookUpRequestOrderArgs { @Field(() => ID, { description: 'ID of the collection', }) + @IsString() + @IsNotEmpty() collectionID: string; @Field(() => ID, { @@ -87,11 +112,15 @@ export class UpdateLookUpRequestOrderArgs { description: 'ID of the request that comes after the updated request in its new position', }) + @IsString() + @IsOptional() nextRequestID: string; @Field(() => ID, { description: 'ID of the request to move', }) + @IsString() + @IsNotEmpty() requestID: string; } @@ -100,5 +129,7 @@ export class GetTeamRequestInCollectionArgs extends PaginationArgs { @Field(() => ID, { description: 'ID of the collection to look in', }) + @IsString() + @IsNotEmpty() collectionID: string; } diff --git a/packages/hoppscotch-backend/src/types/InfraConfig.ts b/packages/hoppscotch-backend/src/types/InfraConfig.ts index 26713b7c142..04876eb0507 100644 --- a/packages/hoppscotch-backend/src/types/InfraConfig.ts +++ b/packages/hoppscotch-backend/src/types/InfraConfig.ts @@ -52,6 +52,8 @@ export enum InfraConfigEnum { VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS', + PROXY_APP_URL = 'PROXY_APP_URL', + ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION', ANALYTICS_USER_ID = 'ANALYTICS_USER_ID', IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP', diff --git a/packages/hoppscotch-backend/src/types/input-types.args.ts b/packages/hoppscotch-backend/src/types/input-types.args.ts index 414b60cf9f6..226694a0c49 100644 --- a/packages/hoppscotch-backend/src/types/input-types.args.ts +++ b/packages/hoppscotch-backend/src/types/input-types.args.ts @@ -1,7 +1,7 @@ import { ArgsType, Field, ID, InputType } from '@nestjs/graphql'; import { ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsNotEmpty, IsOptional } from 'class-validator'; +import { IsInt, IsOptional, IsString, Min } from 'class-validator'; @ArgsType() @InputType() @@ -11,6 +11,8 @@ export class PaginationArgs { defaultValue: undefined, description: 'Cursor for pagination, ID of the last item in the list', }) + @IsString() + @IsOptional() cursor: string; @Field({ @@ -18,14 +20,19 @@ export class PaginationArgs { defaultValue: 10, description: 'Number of items to fetch', }) - take: number; + @IsInt() + @Min(1) + @IsOptional() + @Type(() => Number) + take: number = 10; } @ArgsType() @InputType() export class OffsetPaginationArgs { @IsOptional() - @IsNotEmpty() + @IsInt() + @Min(0) @Type(() => Number) @ApiPropertyOptional() @Field({ @@ -33,10 +40,11 @@ export class OffsetPaginationArgs { defaultValue: 0, description: 'Number of items to skip', }) - skip: number; + skip: number = 0; @IsOptional() - @IsNotEmpty() + @IsInt() + @Min(1) @Type(() => Number) @ApiPropertyOptional() @Field({ @@ -44,5 +52,5 @@ export class OffsetPaginationArgs { defaultValue: 10, description: 'Number of items to fetch', }) - take: number; + take: number = 10; } diff --git a/packages/hoppscotch-backend/src/user-collection/input-type.args.ts b/packages/hoppscotch-backend/src/user-collection/input-type.args.ts index a3d118d1979..e93a3832635 100644 --- a/packages/hoppscotch-backend/src/user-collection/input-type.args.ts +++ b/packages/hoppscotch-backend/src/user-collection/input-type.args.ts @@ -1,10 +1,13 @@ import { Field, ID, ArgsType } from '@nestjs/graphql'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ReqType } from 'src/types/RequestTypes'; import { PaginationArgs } from 'src/types/input-types.args'; @ArgsType() export class CreateRootUserCollectionArgs { @Field({ name: 'title', description: 'Title of the new user collection' }) + @IsString() + @IsNotEmpty() title: string; @Field({ @@ -12,17 +15,23 @@ export class CreateRootUserCollectionArgs { description: 'JSON string representing the collection data', nullable: true, }) + @IsString() + @IsOptional() data: string; } @ArgsType() export class CreateChildUserCollectionArgs { @Field({ name: 'title', description: 'Title of the new user collection' }) + @IsString() + @IsNotEmpty() title: string; @Field(() => ID, { name: 'parentUserCollectionID', description: 'ID of the parent to the new user collection', }) + @IsString() + @IsOptional() parentUserCollectionID: string; @Field({ @@ -30,6 +39,8 @@ export class CreateChildUserCollectionArgs { description: 'JSON string representing the collection data', nullable: true, }) + @IsString() + @IsOptional() data: string; } @@ -39,6 +50,8 @@ export class GetUserChildCollectionArgs extends PaginationArgs { name: 'userCollectionID', description: 'ID of the parent to the user collection', }) + @IsString() + @IsNotEmpty() userCollectionID: string; } @@ -48,12 +61,16 @@ export class RenameUserCollectionsArgs { name: 'userCollectionID', description: 'ID of the user collection', }) + @IsString() + @IsNotEmpty() userCollectionID: string; @Field({ name: 'newTitle', description: 'The updated title of the user collection', }) + @IsString() + @IsNotEmpty() newTitle: string; } @@ -63,6 +80,8 @@ export class UpdateUserCollectionArgs { name: 'collectionID', description: 'ID of collection being moved', }) + @IsString() + @IsNotEmpty() collectionID: string; @Field(() => ID, { @@ -70,6 +89,8 @@ export class UpdateUserCollectionArgs { nullable: true, description: 'ID of collection being moved', }) + @IsString() + @IsOptional() nextCollectionID: string; } @@ -80,12 +101,16 @@ export class MoveUserCollectionArgs { description: 'ID of the parent to the new collection', nullable: true, }) + @IsString() + @IsOptional() destCollectionID: string; @Field(() => ID, { name: 'userCollectionID', description: 'ID of the collection', }) + @IsString() + @IsNotEmpty() userCollectionID: string; } @@ -95,18 +120,25 @@ export class ImportUserCollectionsFromJSONArgs { name: 'jsonString', description: 'JSON string to import', }) + @IsString() + @IsNotEmpty() jsonString: string; + @Field(() => ReqType, { name: 'reqType', description: 'Type of UserCollection', }) + @IsEnum(ReqType) reqType: ReqType; + @Field(() => ID, { name: 'parentCollectionID', description: 'ID to the collection to which to import into (null if to import into the root of the user)', nullable: true, }) + @IsString() + @IsOptional() parentCollectionID?: string; } @@ -116,6 +148,8 @@ export class UpdateUserCollectionsArgs { name: 'userCollectionID', description: 'ID of the user collection', }) + @IsString() + @IsNotEmpty() userCollectionID: string; @Field({ @@ -123,6 +157,8 @@ export class UpdateUserCollectionsArgs { description: 'The updated title of the user collection', nullable: true, }) + @IsString() + @IsOptional() newTitle: string; @Field({ @@ -130,5 +166,7 @@ export class UpdateUserCollectionsArgs { description: 'JSON string representing the collection data', nullable: true, }) + @IsString() + @IsOptional() data: string; } diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts index 7fa2b58db56..e8268009d94 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts @@ -2515,25 +2515,19 @@ describe('importCollectionsFromJSON — collection-level script fields', () => { expect(createCallArg.data.preRequestScript).toBe( 'pw.env.set("ROOT_RAN", "yes");', ); - expect(createCallArg.data.testScript).toBe( - 'pw.test("root", () => {});', - ); + expect(createCallArg.data.testScript).toBe('pw.test("root", () => {});'); const childCreateArg = createCallArg.children.create[0]; expect(childCreateArg.data.preRequestScript).toBe( 'pw.env.set("FOLDER_RAN", "yes");', ); - expect(childCreateArg.data.testScript).toBe( - 'pw.test("folder", () => {});', - ); + expect(childCreateArg.data.testScript).toBe('pw.test("folder", () => {});'); if (E.isRight(result)) { const exported = JSON.parse(result.right.exportedCollection); // `data` is JSON-stringified by transformCollectionData on export. const rootData = JSON.parse(exported[0].data); const folderData = JSON.parse(exported[0].folders[0].data); - expect(rootData.preRequestScript).toBe( - 'pw.env.set("ROOT_RAN", "yes");', - ); + expect(rootData.preRequestScript).toBe('pw.env.set("ROOT_RAN", "yes");'); expect(rootData.testScript).toBe('pw.test("root", () => {});'); expect(folderData.preRequestScript).toBe( 'pw.env.set("FOLDER_RAN", "yes");', diff --git a/packages/hoppscotch-backend/src/user-request/input-type.args.ts b/packages/hoppscotch-backend/src/user-request/input-type.args.ts index b45445bf581..8386269ff47 100644 --- a/packages/hoppscotch-backend/src/user-request/input-type.args.ts +++ b/packages/hoppscotch-backend/src/user-request/input-type.args.ts @@ -1,4 +1,5 @@ import { Field, ID, ArgsType } from '@nestjs/graphql'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { PaginationArgs } from 'src/types/input-types.args'; import { ReqType } from 'src/types/RequestTypes'; @@ -9,6 +10,8 @@ export class GetUserRequestArgs extends PaginationArgs { defaultValue: undefined, description: 'Collection ID of the user request', }) + @IsString() + @IsOptional() collectionID?: string; } @@ -17,16 +20,22 @@ export class MoveUserRequestArgs { @Field(() => ID, { description: 'ID of the collection, where the request is belongs to', }) + @IsString() + @IsNotEmpty() sourceCollectionID: string; @Field(() => ID, { description: 'ID of the request being moved', }) + @IsString() + @IsNotEmpty() requestID: string; @Field(() => ID, { description: 'ID of the collection, where the request is moving to', }) + @IsString() + @IsNotEmpty() destinationCollectionID: string; @Field(() => ID, { @@ -34,6 +43,8 @@ export class MoveUserRequestArgs { description: 'ID of the request that comes after the updated request in its new position', }) + @IsString() + @IsOptional() nextRequestID: string; } @@ -42,24 +53,40 @@ export class CreateUserRequestArgs { @Field(() => ID, { description: 'Collection ID of the user request', }) + @IsString() + @IsNotEmpty() collectionID: string; @Field({ description: 'Title of the user request' }) + @IsString() + @IsNotEmpty() title: string; @Field({ description: 'content/body of the user request' }) + @IsString() + @IsNotEmpty() request: string; - type: ReqType; + @IsOptional() + type?: ReqType; } @ArgsType() export class UpdateUserRequestArgs { + @Field(() => ID, { + description: 'ID of the user request', + }) + @IsString() + @IsNotEmpty() + id: string; + @Field({ nullable: true, defaultValue: undefined, description: 'Title of the user request', }) + @IsString() + @IsOptional() title: string; @Field({ @@ -67,5 +94,7 @@ export class UpdateUserRequestArgs { defaultValue: undefined, description: 'content/body of the user request', }) + @IsString() + @IsOptional() request: string; } diff --git a/packages/hoppscotch-backend/src/user-request/resolvers/user-request.resolver.ts b/packages/hoppscotch-backend/src/user-request/resolvers/user-request.resolver.ts index aaff691af12..bad953ed549 100644 --- a/packages/hoppscotch-backend/src/user-request/resolvers/user-request.resolver.ts +++ b/packages/hoppscotch-backend/src/user-request/resolvers/user-request.resolver.ts @@ -146,16 +146,10 @@ export class UserRequestResolver { @UseGuards(GqlAuthGuard) async updateRESTUserRequest( @GqlUser() user: AuthUser, - @Args({ - name: 'id', - description: 'ID of the user REST request', - type: () => ID, - }) - id: string, @Args() args: UpdateUserRequestArgs, ) { const request = await this.userRequestService.updateRequest( - id, + args.id, args.title, ReqType.REST, args.request, @@ -171,16 +165,10 @@ export class UserRequestResolver { @UseGuards(GqlAuthGuard) async updateGQLUserRequest( @GqlUser() user: AuthUser, - @Args({ - name: 'id', - description: 'ID of the user GraphQL request', - type: () => ID, - }) - id: string, @Args() args: UpdateUserRequestArgs, ) { const request = await this.userRequestService.updateRequest( - id, + args.id, args.title, ReqType.GQL, args.request, diff --git a/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts b/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts index a4e6f9d7a62..bc645c09444 100644 --- a/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts @@ -379,6 +379,7 @@ describe('UserRequestService', () => { const id = userRequests[0].id; const type = userRequests[0].type; const args: UpdateUserRequestArgs = { + id, title: userRequests[0].title, request: userRequests[0].request, }; @@ -401,6 +402,7 @@ describe('UserRequestService', () => { const id = userRequests[0].id; const type = userRequests[0].type; const args: UpdateUserRequestArgs = { + id, title: userRequests[0].title, request: userRequests[0].request, }; @@ -420,7 +422,7 @@ describe('UserRequestService', () => { expect(mockPrisma.userRequest.update).toHaveBeenCalledWith({ where: { id }, data: { - ...args, + title: args.title, request: JSON.parse(args.request), }, }); @@ -429,6 +431,7 @@ describe('UserRequestService', () => { const id = userRequests[0].id; const type = userRequests[0].type; const args: UpdateUserRequestArgs = { + id, title: userRequests[0].title, request: userRequests[0].request, }; @@ -454,6 +457,7 @@ describe('UserRequestService', () => { const id = userRequests[0].id; const type = userRequests[0].type; const args: UpdateUserRequestArgs = { + id, title: userRequests[0].title, request: userRequests[0].request, }; @@ -474,6 +478,7 @@ describe('UserRequestService', () => { const id = userRequests[0].id; const type = userRequests[0].type; const args: UpdateUserRequestArgs = { + id, title: userRequests[0].title, request: 'invalid json', }; diff --git a/packages/hoppscotch-cli/package.json b/packages/hoppscotch-cli/package.json index 03fd3b70343..f86342e761a 100644 --- a/packages/hoppscotch-cli/package.json +++ b/packages/hoppscotch-cli/package.json @@ -52,7 +52,7 @@ "lodash-es": "4.18.1", "papaparse": "5.5.3", "qs": "6.15.1", - "semver": "7.7.4", + "semver": "7.8.0", "tough-cookie": "6.0.1", "verzod": "0.4.0", "xmlbuilder2": "4.0.3", @@ -64,11 +64,11 @@ "@relmify/jest-fp-ts": "2.1.1", "@types/lodash-es": "4.17.12", "@types/papaparse": "5.5.2", - "@types/qs": "6.15.0", + "@types/qs": "6.15.1", "fp-ts": "2.16.11", "prettier": "3.8.3", "tsup": "8.5.1", "typescript": "5.9.3", - "vitest": "4.1.5" + "vitest": "4.1.6" } } diff --git a/packages/hoppscotch-common/languages.json b/packages/hoppscotch-common/languages.json index 5512b184d77..8356912dc08 100644 --- a/packages/hoppscotch-common/languages.json +++ b/packages/hoppscotch-common/languages.json @@ -114,6 +114,12 @@ "iso": "ko-KO", "name": "한국어" }, + { + "code": "mn", + "file": "mn.json", + "iso": "mn-MN", + "name": "Монгол" + }, { "code": "nl", "file": "nl.json", diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index e44b6607cad..f32c1fee9e5 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -760,6 +760,15 @@ }, "export": { "as_json": "Export as JSON", + "as_openapi": "Export as OpenAPI", + "as_yaml": "Export as YAML", + "choose_format": "Choose export format", + "collection": "Export collection", + "collections": "Export collections", + "format_hoppscotch": "Export as Hoppscotch JSON", + "format_openapi_json": "Export as OpenAPI 3.1 (JSON)", + "format_openapi_yaml": "Export as OpenAPI 3.1 (YAML)", + "openapi_lossy_summary": "OpenAPI export drops scripts, auth credentials, inactive items, duplicate paths, and duplicate response status codes.", "create_secret_gist": "Create secret Gist", "create_secret_gist_tooltip_text": "Export as secret Gist", "failed": "Something went wrong while exporting", @@ -1289,7 +1298,8 @@ "delete_account": "Delete account", "delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.", "desktop": "Desktop", - "desktop_description": "Update behavior and keyboard handling for the Hoppscotch desktop app.", + "desktop_description": "Update behavior, keyboard handling, and display preferences for the Hoppscotch desktop app.", + "desktop_display": "Display", "desktop_keyboard": "Keyboard", "desktop_keyboard_strategy_label": "Match shortcuts by typed letter or physical position", "desktop_keyboard_strategy_description": "On non-QWERTY layouts, the same letter can come from different physical keys. The default works for most layouts; switch options if shortcuts don't fire as expected on yours.", @@ -1303,6 +1313,12 @@ "disable_encode_mode_tooltip": "Never encode the parameters in the request", "disable_update_checks": "Disable automatic update checks", "disable_update_checks_description": "Skip the update check at app startup. Use the button above to check on demand.", + "zoom_level": "Zoom level", + "zoom_level_description": "Scales the entire interface. Higher values make text and controls larger on high-resolution screens.", + "zoom_level_100": "100%", + "zoom_level_110": "110%", + "zoom_level_125": "125%", + "zoom_level_150": "150%", "enable_encode_mode_tooltip": "Always encode the parameters in the request", "enter_otp": "Enter Agent's code", "expand_navigation": "Expand navigation", @@ -1331,6 +1347,7 @@ "profile_photo": "Profile photo", "proxy": "Proxy", "proxy_url": "Proxy URL", + "proxy_url_invalid": "Proxy URL must start with http(s):// and contain no spaces", "proxy_use_toggle": "Use the proxy middleware to send requests", "read_the": "Read the", "register_agent": "Register Agent", diff --git a/packages/hoppscotch-common/locales/mn.json b/packages/hoppscotch-common/locales/mn.json new file mode 100644 index 00000000000..0a15e0c4379 --- /dev/null +++ b/packages/hoppscotch-common/locales/mn.json @@ -0,0 +1,2361 @@ +{ + "action": { + "add": "Нэмэх", + "autoscroll": "Автоматаар гүйлгэх", + "cancel": "Цуцлах", + "choose_file": "Файл сонгоно уу", + "choose_workspace": "Ажлын талбарыг сонгоно уу", + "choose_collection": "Цуглуулга сонгох", + "select_workspace": "Ажлын талбарыг сонгоно уу", + "clear": "Тодорхой", + "clear_all": "Бүгдийг арилгах", + "clear_cache": "Кэшийг цэвэрлэх", + "clear_history": "Бүх түүхийг арилгах", + "clear_unpinned": "Тодруулаагүйг арилгах", + "clear_response": "Тодорхой хариулт", + "close": "Хаах", + "confirm": "Баталгаажуулах", + "connect": "Холбох", + "connecting": "Холбож байна", + "copy": "Хуулах", + "create": "Үүсгэх", + "delete": "Устгах", + "disconnect": "Салгах", + "dismiss": "Хаах", + "done": "Дууслаа", + "dont_save": "Хадгалах хэрэггүй", + "download_file": "Файл татаж авах", + "download_test_report": "Туршилтын тайланг татаж авах", + "drag_to_reorder": "Дахин эрэмбэлэхийн тулд чирнэ үү", + "duplicate": "Давхардсан", + "edit": "Засварлах", + "filter": "Шүүлтүүр", + "go_back": "Буцах", + "go_forward": "Урагшаа яв", + "group_by": "Группээр", + "hide_secret": "Нууцыг нуу", + "label": "Шошго", + "learn_more": "Илүү ихийг мэдэж аваарай", + "download_here": "Эндээс татаж авна уу", + "less": "Бага", + "more": "Илүү", + "new": "Шинэ", + "no": "Үгүй", + "open": "Нээлттэй", + "open_workspace": "Нээлттэй ажлын талбар", + "paste": "Буулгах", + "prettify": "Сайхан болгох", + "properties": "Үл хөдлөх хөрөнгө", + "register": "Бүртгүүлэх", + "remove": "Устгах", + "remove_instance": "Жишээ устгах", + "rename": "Нэрээ өөрчлөх", + "restore": "Сэргээх", + "retry": "Дахин оролдоно уу", + "save": "Хадгалах", + "save_as_example": "Жишээ болгон хадгал", + "add_example": "Жишээ нэмнэ үү", + "invalid_request": "Хүсэлтийн өгөгдөл буруу байна", + "scroll_to_bottom": "Доош гүйлгэх", + "scroll_to_top": "Дээд талд гүйлгэх", + "search": "Хайх", + "send": "Илгээх", + "share": "Хуваалцах", + "show_secret": "Нууцыг харуулах", + "sort": "Эрэмбэлэх", + "start": "Эхлэх", + "starting": "Эхэлж байна", + "stop": "Зогс", + "to_close": "хаах", + "to_navigate": "чиглүүлэх", + "to_select": "сонгох", + "turn_off": "Унтраах", + "turn_on": "Асаах", + "undo": "Буцаах", + "unpublish": "Нийтлэлийг цуцлах", + "yes": "Тиймээ", + "verify": "Баталгаажуулах", + "enable": "Идэвхжүүлэх", + "disable": "Идэвхгүй болгох", + "assign": "Даалгах" + }, + "activity_logs": { + "ACTIVITY_LOG_DELETE": "Үйл ажиллагааны бүртгэлийг устгасан", + "WORKSPACE_CREATE": "Шинэ ажлын талбар үүсгэсэн {name}", + "WORKSPACE_RENAME": "Ажлын талбарын нэрийг {old_name}-с {new_name} болгон өөрчилсөн", + "WORKSPACE_USER_ADD": "{user}-г ажлын талбарт {role} гэж нэмсэн", + "WORKSPACE_USER_INVITE": "{user}-г {inviteeEmail} {role} гэж урьсан.", + "WORKSPACE_USER_INVITE_REVOKE": "{inviteeEmail} урилгыг {inviteeRole} гэж цуцалсан", + "WORKSPACE_USER_INVITE_ACCEPT": "{inviteeEmail} урилгыг {inviteeRole} гэж хүлээн авлаа", + "WORKSPACE_USER_REMOVE": "{user}-г ажлын талбараас хассан", + "WORKSPACE_USER_ROLE_UPDATE": "{user}-ийн үүрэг {old_role}-с {new_role} болж шинэчлэгдсэн", + "COLLECTION_CREATE": "Шинэ цуглуулга үүсгэсэн {title}", + "COLLECTION_RENAME": "Цуглуулгын нэрийг {old_title}-с {new_title} болгон өөрчилсөн", + "COLLECTION_IMPORT": "Импортолсон {count} цуглуулга", + "COLLECTION_DELETE": "Устгасан цуглуулга {title}", + "COLLECTION_DUPLICATE": "Давхардсан цуглуулга {parentTitle}", + "REQUEST_CREATE": "Шинэ хүсэлт үүсгэсэн {title}", + "REQUEST_RENAME": "{old_title}-с {new_title} болгон нэрийг нь өөрчилсөн хүсэлт", + "REQUEST_DELETE": "Устгасан хүсэлт {title}" + }, + "add": { + "new": "Шинэ нэмэх", + "star": "Од нэмэх" + }, + "agent": { + "registration_instruction": "Үргэлжлүүлэхийн тулд Hoppscotch Agent-г вэб клиентдээ бүртгүүлнэ үү.", + "enter_otp_instruction": "Hoppscotch Agent-аас үүсгэсэн баталгаажуулах кодыг оруулаад бүртгэлээ дуусгана уу", + "otp_label": "Баталгаажуулах код", + "processing": "Таны хүсэлтийг боловсруулж байна...", + "not_running_title": "Агент илрээгүй", + "registration_title": "Агент бүртгэл", + "verify_ssl_certs": "SSL гэрчилгээг баталгаажуулна уу", + "ca_certs": "CA гэрчилгээ", + "client_certs": "Үйлчлүүлэгчийн гэрчилгээ", + "use_http_proxy": "HTTP прокси ашиглах", + "proxy_capabilities": "Hoppscotch Agent нь HTTP/HTTPS/SOCKS прокси-г NTLM болон Basic Auth-ийн хамт дэмждэг. URL-д прокси баталгаажуулалтын хэрэглэгчийн нэр, нууц үгийг оруулна уу.", + "add_cert_file": "Сертификат файл нэмэх", + "add_client_cert": "Үйлчлүүлэгчийн гэрчилгээ нэмнэ үү", + "add_key_file": "Түлхүүр файл нэмэх", + "domain": "Домэйн", + "cert": "Сертификат", + "key": "Түлхүүр", + "pfx_or_pkcs": "PFX/PKCS12", + "pfx_or_pkcs_file": "PFX/PKCS12 файл", + "add_pfx_or_pkcs_file": "PFX/PKCS12 файл нэмнэ үү" + }, + "app": { + "additional_links": { + "macOS": "macOS", + "windows": "Windows", + "linux": "Линукс", + "web_app": "Вэб програм", + "cli": "CLI" + }, + "downloads": "Татаж авсан зүйлс", + "chat_with_us": "Бидэнтэй чатлаарай", + "contact_us": "Бидэнтэй холбоо барина уу", + "cookies": "Күүки", + "copy": "Хуулах", + "copy_interface_type": "Интерфейсийн төрлийг хуулах", + "copy_user_id": "Хэрэглэгчийн баталгаажуулалтын тэмдгийг хуулах", + "developer_option": "Хөгжүүлэгчийн сонголтууд", + "developer_option_description": "Hoppscotch-ийг хөгжүүлэх, засварлахад тусалдаг хөгжүүлэгчийн хэрэгслүүд.", + "discord": "Discord", + "documentation": "Баримт бичиг", + "github": "GitHub", + "help": "Тусламж, санал хүсэлт", + "home": "Гэр", + "invite": "Урих", + "invite_description": "Hoppscotch бол нээлттэй эхийн API хөгжүүлэлтийн экосистем юм. Бид таны API-г үүсгэх, удирдахад хялбар бөгөөд ойлгомжтой интерфэйсийг зохион бүтээсэн. Hoppscotch нь танд API үүсгэх, турших, баримтжуулах, хуваалцахад туслах хэрэгсэл юм.", + "invite_your_friends": "Найзуудаа урь", + "join_discord_community": "Манай Discord нийгэмлэгт нэгдээрэй", + "keyboard_shortcuts": "Гарын товчлолууд", + "name": "Хоппскоч", + "new_version_found": "Шинэ хувилбар олдсон. Шинэчлэхийн тулд дахин сэргээнэ үү.", + "open_in_hoppscotch": "Hoppscotch-д нээнэ", + "options": "Сонголтууд", + "powered_by": "Powered by Hoppscotch", + "proxy_privacy_policy": "Прокси нууцлалын бодлого", + "reload": "Дахин ачаалах", + "search": "Хайлт ба тушаалууд", + "share": "Хуваалцах", + "shortcuts": "Товчлолууд", + "social_description": "Хамгийн сүүлийн үеийн мэдээ, шинэчлэлт, хувилбаруудыг цаг тухайд нь авахын тулд бидэнтэй нийгмийн сүлжээг дага.", + "social_links": "Нийгмийн холбоосууд", + "spotlight": "Анхаарал татахуйц", + "status": "Статус", + "status_description": "Сайтын статусыг шалгана уу", + "terms_and_privacy": "Нөхцөл ба нууцлал", + "twitter": "Twitter", + "type_a_command_search": "Тушаал бичих эсвэл хайх...", + "we_use_cookies": "Бид күүки ашигладаг", + "updated_text": "Hoppscotch v{version}🎉 болж шинэчлэгдсэн", + "whats_new": "Шинэ зүйл юу вэ?", + "see_whats_new": "Шинэ зүйл юу байгааг хараарай", + "wiki": "Вики", + "collapse_sidebar": "Хажуугийн самбарыг буулгах", + "continue_to_dashboard": "Хяналтын самбар руу үргэлжлүүлнэ үү", + "expand_sidebar": "Хажуугийн самбарыг өргөжүүлэх", + "no_name": "Нэргүй", + "open_navigation": "Навигацийг нээх", + "read_documentation": "Баримт бичгийг уншина уу", + "default": "анхдагч: {value}" + }, + "auth": { + "account_deactivated": "Таны бүртгэл идэвхгүй болсон байна. Хандалт авахын тулд админтай холбогдоно уу.", + "account_exists": "Бүртгэл өөр өөр үнэмлэхтэй байна - Хоёр бүртгэлийг холбохын тулд нэвтэрнэ үү", + "all_sign_in_options": "Бүх нэвтрэх сонголтууд", + "continue_with_auth_provider": "{provider}-р үргэлжлүүлэх", + "continue_with_email": "Цахим шуудангаар үргэлжлүүлнэ үү", + "continue_with_github": "GitHub-г үргэлжлүүлнэ үү", + "continue_with_github_enterprise": "GitHub Enterprise-тэй үргэлжлүүлнэ үү", + "continue_with_google": "Google-тэй үргэлжлүүлнэ үү", + "continue_with_microsoft": "Майкрософттой үргэлжлүүлнэ үү", + "email": "Имэйл", + "logged_out": "Гарсан", + "login": "Нэвтрэх", + "login_success": "Амжилттай нэвтэрлээ", + "login_to_hoppscotch": "Hoppscotch руу нэвтэрнэ үү", + "logout": "Гарах", + "re_enter_email": "Имэйлээ дахин оруулна уу", + "send_magic_link": "Шидэт холбоос илгээнэ үү", + "sync": "Синк хийх", + "we_sent_magic_link": "Бид танд ид шидийн холбоос илгээсэн!", + "we_sent_magic_link_description": "Ирсэн имэйлээ шалгана уу - бид {email} руу имэйл илгээсэн. Энэ нь таныг нэвтрэх шидэт холбоосыг агуулдаг." + }, + "authorization": { + "generate_token": "Токен үүсгэх", + "refresh_token": "Токеныг шинэчлэх", + "graphql_headers": "Зөвшөөрлийн толгойг холболтын_init рүү ачааллын нэг хэсэг болгон илгээдэг", + "include_in_url": "URL-д оруулна уу", + "inherited_from": "Эцэг эхийн цуглуулгаас {collection} өвлөн авсан {auth}", + "learn": "Хэрхэн гэдгийг мэдэж аваарай", + "oauth": { + "redirect_auth_server_returned_error": "Auth сервер алдааны төлөвийг буцаасан", + "redirect_auth_token_request_failed": "Баталгаажуулах токен авах хүсэлт амжилтгүй боллоо", + "redirect_auth_token_request_invalid_response": "Баталгаажуулах токен хүсэх үед Токен төгсгөлийн цэгийн хариу буруу байна", + "redirect_invalid_state": "Дахин чиглүүлэхэд буруу төлөвийн утга байна", + "redirect_no_auth_code": "Дахин чиглүүлэхэд Зөвшөөрлийн код байхгүй байна", + "redirect_no_client_id": "Үйлчлүүлэгчийн ID-г тодорхойлоогүй байна", + "redirect_no_client_secret": "Үйлчлүүлэгчийн нууцыг тодорхойлоогүй", + "redirect_no_code_verifier": "Код баталгаажуулагчийг тодорхойлоогүй байна", + "redirect_no_token_endpoint": "Токен төгсгөлийн цэг тодорхойлогдоогүй", + "something_went_wrong_on_oauth_redirect": "OAuth дахин чиглүүлэх явцад алдаа гарлаа", + "something_went_wrong_on_token_generation": "Токен үүсгэхэд алдаа гарлаа", + "token_generation_oidc_discovery_failed": "Токен үүсгэхэд алдаа гарсан: OpenID Connect нээлт амжилтгүй боллоо", + "grant_type": "Тэтгэлгийн төрөл", + "grant_type_auth_code": "Зөвшөөрлийн код", + "token_fetched_successfully": "Токеныг амжилттай татаж авлаа", + "token_fetch_failed": "Токеныг дуудаж чадсангүй", + "validation_failed": "Баталгаажуулалт амжилтгүй болсон тул маягтын талбаруудыг шалгана уу", + "no_refresh_token_present": "Сэргээх токен байхгүй байна. Токен үүсгэх урсгалыг дахин ажиллуулна уу", + "refresh_token_request_failed": "Токен сэргээх хүсэлт амжилтгүй боллоо", + "token_refreshed_successfully": "Токеныг амжилттай шинэчилсэн", + "label_authorization_endpoint": "Зөвшөөрлийн төгсгөлийн цэг", + "label_client_id": "Үйлчлүүлэгчийн ID", + "label_client_secret": "Үйлчлүүлэгчийн нууц", + "label_code_challenge": "Код сорилт", + "label_code_challenge_method": "Код сорилтын арга", + "label_code_verifier": "Код баталгаажуулагч", + "label_scopes": "Хамрах хүрээ", + "label_token_endpoint": "Токен төгсгөлийн цэг", + "label_use_pkce": "PKCE ашиглах", + "label_implicit": "Далд", + "label_password": "Нууц үг", + "label_username": "Хэрэглэгчийн нэр", + "label_auth_code": "Зөвшөөрлийн код", + "label_client_credentials": "Үйлчлүүлэгчийн итгэмжлэл", + "label_send_as": "Үйлчлүүлэгчийн баталгаажуулалт", + "label_send_in_body": "Итгэмжлэх жуух бичгээ биедээ илгээнэ үү", + "label_send_as_basic_auth": "Итгэмжлэх жуух бичгийг үндсэн баталгаажуулалтаар илгээнэ үү", + "enter_value": "Утга оруулна уу", + "auth_request": "Баталгаажуулах хүсэлт", + "token_request": "Токен хүсэлт", + "refresh_request": "Хүсэлтийг сэргээх", + "send_in": "Илгээх" + }, + "pass_key_by": "Хажуугаар нь өнгөр", + "pass_by_query_params_label": "Асуулгын параметрүүд", + "pass_by_headers_label": "Гарчиг", + "password": "Нууц үг", + "save_to_inherit": "Зөвшөөрлийг өвлүүлэхийн тулд энэ хүсэлтийг дурын цуглуулгад хадгална уу", + "token": "Токен", + "access_token": "Токен руу нэвтрэх", + "client_token": "Үйлчлүүлэгчийн токен", + "client_secret": "Үйлчлүүлэгчийн нууц", + "timestamp": "Цагийн тэмдэг", + "host": "Хөтлөгч", + "type": "Зөвшөөрлийн төрөл", + "username": "Хэрэглэгчийн нэр", + "advance_config": "Нарийвчилсан тохиргоо", + "advance_config_description": "Хэрэв тодорхой утга байхгүй бол Hoppscotch автоматаар тодорхой талбарт өгөгдмөл утгыг оноодог", + "algorithm": "Алгоритм", + "payload": "Ачаалал", + "secret": "Нууц", + "aws_signature": { + "access_key": "Хандалтын түлхүүр", + "secret_key": "Нууц түлхүүр", + "service_name": "Үйлчилгээний нэр", + "aws_region": "AWS бүс", + "service_token": "Үйлчилгээний токен" + }, + "digest": { + "realm": "Хүрээ", + "nonce": "Үгүй ээ", + "algorithm": "Алгоритм", + "qop": "qop", + "nonce_count": "Nonce Count", + "client_nonce": "Үйлчлүүлэгч Нонс", + "opaque": "Тунгалаг", + "disable_retry": "Дахин оролдох хүсэлтийг идэвхгүй болгох" + }, + "akamai": { + "headers_to_sign": "Гарын үсэг зурах", + "max_body_size": "Хамгийн их биеийн хэмжээ" + }, + "hawk": { + "id": "HAWK баталгаажуулалтын ID", + "key": "HAWK баталгаажуулах түлхүүр", + "ext": "ext", + "app": "апп", + "dlg": "dlg", + "include": "Ачааны хэшийг оруулна уу" + }, + "jwt": { + "params_name": "Парамсын нэр", + "param_name": "Параметрийн нэр", + "header_prefix": "Толгой хэсгийн угтвар", + "placeholder_request_header": "Толгой хэсгийн угтварыг хүсэх", + "placeholder_request_param": "Парамсын нэрийг хүсэх", + "secret_base64_encoded": "Нууц суурь64 кодлогдсон", + "headers": "JWT толгой", + "private_key": "Хувийн түлхүүр", + "placeholder_headers": "JWT толгой" + }, + "ntlm": { + "domain": "Домэйн", + "workstation": "Ажлын станц", + "disable_retrying_request": "Дахин оролдох хүсэлтийг идэвхгүй болгох" + }, + "asap": { + "issuer": "Үнэт цаас гаргагч", + "audience": "Үзэгчид", + "expires_in": "Дуусах хугацаа", + "key_id": "Түлхүүр ID", + "optional_config": "Нэмэлт тохиргоо", + "subject": "Сэдэв", + "additional_claims": "Нэмэлт нэхэмжлэл" + } + }, + "collection": { + "title": "Цуглуулга", + "run": "Цуглуулга ажиллуулах", + "created": "Цуглуулга үүсгэсэн", + "different_parent": "Өөр эцэг эхтэй цуглуулгыг дахин захиалах боломжгүй", + "edit": "Цуглуулга засварлах", + "import_or_create": "Импортлох эсвэл цуглуулга үүсгэх", + "import_collection": "Импортын цуглуулга", + "invalid_name": "Цуглуулгын нэрийг оруулна уу", + "invalid_root_move": "Цуглуулга аль хэдийн үндэст байна", + "moved": "Амжилттай хөдөлсөн", + "my_collections": "Хувийн цуглуулгууд", + "name": "Миний шинэ цуглуулга", + "name_length_insufficient": "Цуглуулгын нэр дор хаяж 3 тэмдэгт байх ёстой", + "new": "Шинэ цуглуулга", + "order_changed": "Цуглуулгын захиалга шинэчлэгдсэн", + "properties": "Цуглуулгын шинж чанарууд", + "properties_updated": "Цуглуулгын шинж чанарууд шинэчлэгдсэн", + "renamed": "Цуглуулгын нэрийг өөрчилсөн", + "request_in_use": "Ашиглаж байгаа хүсэлт", + "save_as": "Хадгалах", + "save_to_collection": "Цуглуулгад хадгалах", + "select": "Цуглуулга сонгоно уу", + "select_location": "Байршлыг сонгоно уу", + "sorted": "Цуглуулгыг эрэмбэлсэн", + "details": "Дэлгэрэнгүй мэдээлэл", + "duplicated": "Цуглуулга давхардсан" + }, + "confirm": { + "close_unsaved_tab": "Та энэ табыг хаахдаа итгэлтэй байна уу?", + "close_unsaved_tabs": "Та бүх табыг хаахдаа итгэлтэй байна уу? {count} хадгалагдаагүй табууд устах болно.", + "delete_all_activity_logs": "Та бүх үйл ажиллагааны бүртгэлийг устгахдаа итгэлтэй байна уу?", + "exit_team": "Та энэ ажлын талбараас гарахдаа итгэлтэй байна уу?", + "logout": "Та гарахдаа итгэлтэй байна уу?", + "remove_collection": "Та энэ цуглуулгыг бүрмөсөн устгахдаа итгэлтэй байна уу?", + "remove_environment": "Та энэ орчныг бүрмөсөн устгахдаа итгэлтэй байна уу?", + "remove_folder": "Та энэ фолдерыг бүрмөсөн устгахдаа итгэлтэй байна уу?", + "remove_history": "Та бүх түүхийг бүрмөсөн устгахдаа итгэлтэй байна уу?", + "remove_request": "Та энэ хүсэлтийг бүрмөсөн устгахдаа итгэлтэй байна уу?", + "remove_response": "Та энэ хариултыг бүрмөсөн устгахдаа итгэлтэй байна уу?", + "remove_shared_request": "Та энэ хуваалцсан хүсэлтийг бүрмөсөн устгахдаа итгэлтэй байна уу?", + "remove_team": "Та энэ ажлын талбарыг устгахдаа итгэлтэй байна уу?", + "remove_telemetry": "Та Telemetry-ээс татгалзахдаа итгэлтэй байна уу?", + "request_change": "Та одоогийн хүсэлтийг цуцлахдаа итгэлтэй байна уу, хадгалаагүй өөрчлөлтүүд устах болно.", + "save_unsaved_tab": "Та энэ таб дээр хийсэн өөрчлөлтийг хадгалахыг хүсэж байна уу?", + "sync": "Та ажлын талбараа үүлнээс сэргээхийг хүсч байна уу? Энэ нь таны орон нутгийн ахиц дэвшлийг устгах болно.", + "delete_access_token": "Та {tokenLabel} хандалтын токеныг устгахдаа итгэлтэй байна уу?", + "delete_mock_server": "Та энэ хуурамч серверийг устгахдаа итгэлтэй байна уу?" + }, + "context_menu": { + "add_parameters": "Параметрт нэмэх", + "open_request_in_new_tab": "Хүсэлтийг шинэ таб дээр нээнэ үү", + "set_environment_variable": "Хувьсагчаар тохируулна уу", + "encode_uri_component": "URL бүрэлдэхүүн хэсгийг кодлох", + "decode_uri_component": "URL бүрэлдэхүүн хэсгийг тайлах" + }, + "cookies": { + "modal": { + "cookie_expires": "Хугацаа нь дуусна", + "cookie_name": "Нэр", + "cookie_path": "Зам", + "cookie_string": "Күүки мөр", + "cookie_value": "Үнэ цэнэ", + "empty_domain": "Домэйн хоосон байна", + "empty_domains": "Домэйн жагсаалт хоосон байна", + "enter_cookie_string": "Күүкийн мөрийг оруулна уу", + "interceptor_no_support": "Таны одоо сонгосон таслан зогсоох төхөөрөмж күүкиг дэмждэггүй. Өөр Interceptor сонгоод дахин оролдоно уу.", + "managed_tab": "Удирдсан", + "new_domain_name": "Шинэ домэйн нэр", + "no_cookies_in_domain": "Энэ домэйнд күүки тохируулаагүй байна", + "raw_tab": "Түүхий", + "set": "Күүки тавь" + } + }, + "count": { + "currentValue": "Одоогийн утга {count}", + "description": "Тодорхойлолт {count}", + "header": "Гарчиг {count}", + "initialValue": "Анхны утга {count}", + "key": "Түлхүүр {count}", + "message": "Зурвас {count}", + "parameter": "Параметр {count}", + "protocol": "Протокол {count}", + "value": "Утга {count}", + "variable": "Хувьсагч {count}" + }, + "documentation": { + "add_description": "Энэ цуглуулгын тайлбарыг энд нэмнэ үү...", + "add_description_placeholder": "Энд тайлбар нэмнэ үү...", + "add_request_description": "Хүсэлтийн тайлбарыг энд нэмнэ үү...", + "auth": { + "access_key": "Хандалтын түлхүүр", + "access_token": "Токен руу нэвтрэх", + "add_to": "-д нэмэх", + "akamai_edgegrid": "Akamai EdgeGrid", + "algorithm": "Алгоритм", + "api_key": "API түлхүүр", + "app_id": "Апп ID", + "auth_id": "Баталгаажуулах ID", + "auth_key": "Баталгаажуулах түлхүүр", + "auth_url": "Auth URL", + "aws_signature": "AWS гарын үсэг", + "basic_auth": "Үндсэн зохиогч", + "bearer_token": "Баригч токен", + "client_id": "Үйлчлүүлэгчийн ID", + "client_nonce": "Үйлчлүүлэгч Нонс", + "client_secret": "Үйлчлүүлэгчийн нууц", + "client_token": "Үйлчлүүлэгчийн токен", + "delegation": "Төлөөлөгч", + "digest_auth": "Digest Auth", + "extra_data": "Нэмэлт өгөгдөл", + "grant_type": "Тэтгэлгийн төрөл", + "hawk_auth": "HAWK Auth", + "headers_to_sign": "Гарын үсэг зурах", + "host": "Хөтлөгч", + "include_payload_hash": "Ачааны хэшийг оруулна уу", + "jwt_auth": "JWT Auth", + "max_body_size": "Хамгийн их биеийн хэмжээ", + "no_auth": "Баталгаажуулалт байхгүй", + "nonce": "Үгүй ээ", + "oauth_2": "OAuth 2.0", + "opaque": "Тунгалаг", + "password": "Нууц үг", + "payload": "Ачаалал", + "qop": "QOP", + "realm": "Хүрээ", + "region": "Бүс нутаг", + "scope": "Хамрах хүрээ", + "secret_key": "Нууц түлхүүр", + "service_name": "Үйлчилгээний нэр", + "timestamp": "Цагийн тэмдэг", + "title": "Баталгаажуулалт", + "token_url": "Токен URL", + "user": "Хэрэглэгч", + "username": "Хэрэглэгчийн нэр" + }, + "body": { + "content_type": "Агуулгын төрөл", + "no_body": "Тодорхойлсон бие байхгүй", + "title": "Бие" + }, + "copied_to_clipboard": "Түр санах ой руу хуулсан!", + "curl": { + "click_to_load": "cURL командыг ачаалах бол товшино уу", + "copied": "cURL командыг санах ой руу хуулсан!", + "copy_to_clipboard": "Түр санах ойд хуулах", + "generating": "cURL командыг үүсгэж байна...", + "load": "cURL-г ачаална уу", + "title": "cURL" + }, + "description": "Тодорхойлолт", + "error_rendering_markdown": "Тэмдэглэгээг буулгахад алдаа гарлаа:", + "fetching_documentation": "Баримт бичгийг авчирч байна...", + "generate": "Баримт бичиг үүсгэх", + "generate_message": "Явж байхдаа API баримт бичгийг үүсгэхийн тулд дурын Hoppscotch цуглуулгыг импортлох.", + "headers": { + "no_headers": "Тодорхойлолтгүй гарчиг", + "title": "Гарчиг" + }, + "hide_all_documentation": "Бүх баримт бичгийг нуух", + "inherited_from": "{name}-аас өвлөн авсан", + "inherited_with_type": "{type}-г {name}-аас өвлөн авсан", + "key": "Түлхүүр", + "loading": "Ачааж байна...", + "loading_collection_data": "Цуглуулгын өгөгдлийг ачаалж байна...", + "no": "Үгүй", + "no_collection_data": "Цуглуулгын мэдээлэл байхгүй байна", + "no_documentation_found": "Фолдер эсвэл хүсэлтийн баримт бичиг олдсонгүй", + "no_request_data": "Хүсэлтийн өгөгдөл байхгүй байна", + "no_requests_or_folders": "Хүсэлт, хавтас байхгүй", + "not_set": "Тохируулаагүй", + "open_request_in_new_tab": "Хүсэлтийг шинэ таб дээр нээнэ үү", + "parameters": { + "no_params": "Тодорхойлогдсон параметр байхгүй", + "title": "Параметрүүд" + }, + "percent_complete": "% дууссан", + "processing_documentation": "Баримт бичгийг боловсруулах", + "publish": { + "already_published": "Энэ цуглуулга аль хэдийн хэвлэгдсэн байна", + "auto_sync": "Цуглуулгатай автоматаар синк хийх", + "auto_sync_description": "Цуглуулга өөрчлөгдөх үед нийтлэгдсэн баримтуудыг автоматаар шинэчлэх", + "button": "Нийтлэх", + "copy_url": "URL хуулах", + "delete": "Баримт бичгийг устгах", + "unpublish_doc": "Та баримт бичгийг нийтлэхээ болиулахдаа итгэлтэй байна уу?", + "delete_success": "Нийтэлсэн баримт бичгийг амжилттай устгалаа", + "doc_title": "Гарчиг", + "doc_version": "Хувилбар", + "edit_published_doc": "Нийтэлсэн баримт бичгийг засварлах", + "last_updated": "Сүүлд шинэчлэгдсэн", + "metadata": "Мета өгөгдөл (JSON)", + "open_published_doc": "Нийтэлсэн баримт бичгийг шинэ таб дээр нээнэ үү", + "publish_error": "Баримт бичгийг нийтэлж чадсангүй", + "publish_success": "Баримт бичгийг амжилттай нийтлэв!", + "published": "Нийтэлсэн", + "published_url": "Нийтэлсэн URL", + "title": "Баримт бичгийг нийтлэх", + "update_button": "Шинэчлэх", + "update_error": "Баримт бичгийг шинэчилж чадсангүй", + "update_published_docs": "Нийтэлсэн баримт бичгүүдийг шинэчлэх", + "update_success": "Баримт бичгийг амжилттай шинэчилсэн!", + "unpublish": "Нийтлэлийг цуцлах", + "update_title": "Нийтэлсэн баримт бичгийг шинэчлэх", + "url_copied": "URL-г санах ой руу хуулсан!", + "view_published": "Нийтэлсэн баримтуудыг үзэх", + "view_title": "Нийтэлсэн баримт бичгийн агшин зураг", + "versions": "Хувилбарууд", + "create_new_version": "Шинэ хувилбар үүсгэх", + "invalid_version": "Хувилбар нь зөвхөн үсэг, тоон тэмдэгт, цэг, зураас агуулсан байх ёстой", + "live": "Амьд", + "snapshot": "Хормын хувилбар", + "version_immutable": "Нийтлэгдсэн хувилбарууд нь зөвхөн унших боломжтой хормын хувилбарууд юм", + "view_snapshot": "Snapshot үзэх", + "current_version": "ОДОО", + "not_found": "Нийтэлсэн баримт бичиг олдсонгүй", + "no_doc_id": "Баримт бичгийн ID өгөөгүй", + "version_label": "v{version}", + "unpublish_version": "Энэ хувилбарыг нийтлээгүй", + "loading_snapshot": "Хормын хувилбарыг ачаалж байна...", + "retry_snapshot": "Дахин оролдоно уу", + "sensitive_data_warning": "Нийтлэгдсэн баримт бичигт ямар ч нууц мэдээлэл ил гарсан эсэхийг шалгана уу.", + "snapshot_preview": "Хормын хувилбарыг урьдчилан харах", + "snapshot_load_error": "Хормын хувилбарыг урьдчилан үзэхийг ачаалж чадсангүй", + "snapshot_empty": "Энэ хормын хувилбарт хүсэлт эсвэл хавтас алга", + "snapshot_item_count": "{count} зүйл", + "auto_sync_live_notice": "Энэ хувилбар нь шууд цуглуулгатай автоматаар синк хийдэг", + "snapshot_promote_warning": "Автомат синкийг идэвхжүүлснээр энэ царцаасан агшин агшныг шууд цуглуулгын модоор солих болно. Үүнийг буцаах боломжгүй.", + "live_freeze_notice": "Автомат синкийг унтрааж, энэ хувилбарыг одоогийн цуглуулгын төлөвт царцаах болно.", + "untitled_project": "Гарчиггүй төсөл", + "environment": "Байгаль орчин", + "no_environment": "Орчин байхгүй", + "environment_description": "Нийтлэгдсэн баримт бичигт хувьсагчдыг шийдвэрлэх орчныг хавсаргана уу" + }, + "request_opened_in_new_tab": "Хүсэлтийг шинэ таб дээр нээсэн!", + "response": { + "body": "Хариу өгөх байгууллага", + "copy": "Хариултыг хуулах", + "example_copied": "Хариултын жишээг санах ой руу хуулсан!", + "example_copy_failed": "Хариултын жишээг хуулж чадсангүй", + "headers": "Хариултын толгой", + "no_examples": "Хариултын жишээ байхгүй", + "title": "Хариултын жишээ" + }, + "save_error": "Баримт бичгийг хадгалахад алдаа гарлаа", + "save_success": "Баримт бичгийг амжилттай хадгаллаа", + "saved_items_status": "{success} зүйлийг хадгалсан.{failure} зүйлийг хадгалж чадсангүй.", + "show_all_documentation": "Бүх баримт бичгийг харуулах", + "source": "Эх сурвалж", + "title": "Баримт бичиг", + "unsaved_changes": "Танд хадгалагдаагүй {count} өөрчлөлт байна. Хаахаасаа өмнө хадгална уу.", + "untitled_collection": "Гарчиггүй цуглуулга", + "untitled_request": "Гарчиггүй хүсэлт", + "value": "Үнэ цэнэ", + "variables": { + "no_vars": "Тодорхойлсон хувьсагч байхгүй", + "title": "Хувьсагч" + }, + "yes": "Тиймээ" + }, + "empty": { + "activity_logs": "Үйл ажиллагааны бүртгэл олдсонгүй", + "authorization": "Энэ хүсэлт нь ямар ч зөвшөөрөл ашиглахгүй", + "body": "Энэ хүсэлт нь биетэй байдаггүй", + "collection": "Цуглуулга хоосон байна", + "collections": "Цуглуулга хоосон байна", + "documentation": "Баримт бичгийг үзэхийн тулд GraphQL төгсгөлийн цэгт холбогдоно уу", + "empty_schema": "Схем олдсонгүй", + "endpoint": "Төгсгөлийн цэг хоосон байж болохгүй", + "environments": "Байгаль орчин хоосон байна", + "collection_variables": "Цуглуулгын хувьсагчид хоосон байна", + "folder": "Хавтас хоосон байна", + "headers": "Энэ хүсэлтэд гарчиг байхгүй", + "history": "Түүх хоосон байна", + "invites": "Урилгын жагсаалт хоосон байна", + "members": "Ажлын талбар хоосон байна", + "parameters": "Энэ хүсэлтэд ямар ч параметр байхгүй", + "pending_invites": "Энэ ажлын талбарт хүлээгдэж буй урилга алга", + "profile": "Өөрийн профайлыг үзэхийн тулд нэвтэрнэ үү", + "protocols": "Протоколууд хоосон байна", + "request_variables": "Энэ хүсэлтэд хүсэлтийн хувьсагч байхгүй байна", + "schema": "Схемийг үзэхийн тулд GraphQL төгсгөлийн цэгт холбогдоно уу", + "search_environment": "Тохирох орчин олдсонгүй", + "secret_environments": "Нууцуудыг Hoppscotch-тэй синк хийдэггүй", + "shared_requests": "Хуваалцсан хүсэлтүүд хоосон байна", + "shared_requests_logout": "Хуваалцсан хүсэлтээ харах эсвэл шинээр үүсгэхийн тулд нэвтэрнэ үү", + "subscription": "Захиалга хоосон байна", + "team_name": "Ажлын талбарын нэр хоосон байна", + "teams": "Та ямар ч ажлын талбарт харьяалагдахгүй", + "tests": "Энэ хүсэлтийн хувьд туршилт байхгүй байна", + "access_tokens": "Хандалтын токенууд хоосон байна", + "response": "Хариу ирээгүй", + "mock_servers": "Хуурамч сервер олдсонгүй" + }, + "environment": { + "heading": "Байгаль орчин", + "add_to_global": "Глобал руу нэмнэ үү", + "added": "Орчны хувьсагч нэмэгдсэн", + "create_new": "Шинэ орчин бүрдүүлэх", + "created": "Бүтээсэн орчин", + "current_value": "Одоогийн үнэ цэнэ", + "deleted": "Орчны хувьсагчийг устгасан", + "duplicated": "Хүрээлэн буй орчин давхардсан", + "edit": "Байгаль орчныг засах", + "empty_variables": "Хувьсагч байхгүй", + "global": "Глобал", + "global_variables": "Глобал хувьсагч", + "import_or_create": "Импортлох эсвэл орчин бүрдүүлэх", + "initial_value": "Анхны үнэ цэнэ", + "invalid_name": "Орчныхоо нэрийг оруулна уу", + "list": "Хүрээлэн буй орчны хувьсагчид", + "my_environments": "Хувийн орчин", + "name": "Нэр", + "nested_overflow": "Оруулсан орчны хувьсагчдыг 10 түвшинд хязгаарласан", + "new": "Шинэ орчин", + "no_active_environment": "Идэвхтэй орчин байхгүй", + "no_environment": "Орчин байхгүй", + "no_environment_description": "Ямар ч орчин сонгогдоогүй. Дараах хувьсагчид юу хийхээ сонгоно уу.", + "quick_peek": "Байгаль орчныг хурдан харах", + "replace_all_current_with_initial": "Бүх гүйдлийг эхнийхээр солино", + "replace_all_initial_with_current": "Бүх эхлэлийг гүйдэлээр солино", + "replace_current_with_initial": "Эхлэлээр солино", + "replace_initial_with_current": "Гүйдлээр солих", + "replace_with_variable": "Хувьсагчаар солино", + "scope": "Хамрах хүрээ", + "secrets": "Нууцууд", + "secret_value": "Нууц үнэ цэнэ", + "select": "Орчноо сонгох", + "set": "Орчноо тохируулах", + "set_as_environment": "Орчин болгон тохируулна уу", + "short_name": "Орчны нэр дор хаяж 1 тэмдэгттэй байх шаардлагатай", + "team_environments": "Ажлын байрны орчин", + "title": "Байгаль орчин", + "updated": "Байгаль орчин шинэчлэгдсэн", + "value": "Үнэ цэнэ", + "variable": "Хувьсагч", + "variables": "Хувьсагч", + "variable_list": "Хувьсагчийн жагсаалт", + "properties": "Байгаль орчны шинж чанарууд", + "details": "Дэлгэрэнгүй мэдээлэл" + }, + "error": { + "network": { + "heading": "Сүлжээний алдаа", + "description": "Сүлжээний холболт амжилтгүй боллоо. {message}: {cause}" + }, + "timeout": { + "heading": "Хугацаа хэтэрсэн алдаа", + "description": "{phase} үед хүсэлтийн хугацаа дууссан. {message}" + }, + "certificate": { + "heading": "Сертификатын алдаа", + "description": "Хүчингүй гэрчилгээ. {message}: {cause}" + }, + "auth": { + "heading": "Баталгаажуулалтын алдаа", + "description": "Хандалтыг хориглосон. {message}: {cause}" + }, + "proxy": { + "heading": "Прокси алдаа", + "description": "Прокси холболт амжилтгүй боллоо. {message}: {cause}" + }, + "parse": { + "heading": "Шинжилгээний алдаа", + "description": "Хариултыг задлан шинжилж чадсангүй. {message}: {cause}" + }, + "version": { + "heading": "Хувилбарын алдаа", + "description": "Тохиромжгүй хувилбарууд. {message}: {cause}" + }, + "abort": { + "heading": "Хүсэлтийг цуцалсан", + "description": "Үйл ажиллагааг цуцалсан. {message}: {cause}" + }, + "unknown": { + "heading": "Үл мэдэгдэх алдаа", + "description": "Үл мэдэгдэх алдаа гарлаа.", + "cause": "Үл мэдэгдэх шалтгаан" + }, + "extension": { + "heading": "Өргөтгөлийн алдаа", + "description": "Өргөтгөл дээр ажиллах хүсэлт амжилтгүй боллоо" + }, + "authproviders_load_error": "Баталгаажуулах үйлчилгээ үзүүлэгчдийг ачаалах боломжгүй", + "browser_support_sse": "Энэ хөтөч нь Server Sent Events-ийг дэмждэггүй бололтой.", + "check_console_details": "Дэлгэрэнгүй мэдээллийг консолын бүртгэлээс шалгана уу.", + "check_how_to_add_origin": "Гарал үүслийг хэрхэн нэмэх боломжтойг шалгана уу", + "curl_invalid_format": "cURL зөв форматлагдаагүй байна", + "danger_zone": "Аюултай бүс", + "delete_account": "Таны бүртгэл одоогоор эдгээр ажлын талбарт цорын ганц эзэмшигч байна:", + "delete_account_description": "Та бүртгэлээ устгахаасаа өмнө өөрийгөө устгах, өмчлөх эрхийг шилжүүлэх эсвэл эдгээр ажлын талбарыг устгах ёстой.", + "delete_activity_log": "Үйл ажиллагааны бүртгэлийг устгаж чадсангүй", + "delete_all_activity_logs": "Бүх үйл ажиллагааны бүртгэлийг устгаж чадсангүй", + "email_already_exists": "Энэ имэйл өөр бүртгэлтэй байна. Өөр имэйл хаяг тохируулна уу.", + "empty_email_address": "Имэйл хаяг хоосон байж болохгүй", + "empty_profile_name": "Профайлын нэр хоосон байж болохгүй", + "empty_req_name": "Хоосон хүсэлтийн нэр", + "fetch_activity_logs": "Үйл ажиллагааны бүртгэлийг дуудаж чадсангүй", + "f12_details": "(Дэлгэрэнгүйг F12)", + "gql_prettify_invalid_query": "Хүчингүй асуулгыг сайжруулж чадсангүй, асуулгын синтаксийн алдааг шийдэж, дахин оролдоно уу", + "incomplete_config_urls": "Бүрэн бус тохиргооны URL", + "incorrect_email": "Буруу имэйл", + "invalid_file_type": "`{filename}` файлын төрөл буруу.", + "invalid_link": "Буруу холбоос", + "invalid_link_description": "Таны товшсон холбоос хүчингүй эсвэл хугацаа нь дууссан байна.", + "invalid_embed_link": "Оруулсан зүйл байхгүй эсвэл буруу байна.", + "json_parsing_failed": "Буруу JSON", + "json_prettify_invalid_body": "Буруу биеийг гоёж чадсангүй, json синтакс алдааг шийдэж, дахин оролдоно уу", + "network_error": "Сүлжээнд алдаа гарсан бололтой. Дахин оролдоно уу.", + "network_fail": "Хүсэлт илгээж чадсангүй", + "no_collections_to_export": "Экспортлох цуглуулга байхгүй. Эхлэхийн тулд цуглуулга үүсгэнэ үү.", + "no_duration": "Үргэлжлэх хугацаа байхгүй", + "no_environments_to_export": "Экспортлох орчин байхгүй. Эхлэхийн тулд орчинг бүрдүүлнэ үү.", + "no_results_found": "Тохирох зүйл олдсонгүй", + "page_not_found": "Энэ хуудсыг олж чадсангүй", + "please_install_extension": "Өргөтгөлийг суулгаж, өргөтгөлийн эх сурвалжийг нэмнэ үү.", + "proxy_error": "Прокси алдаа", + "same_email_address": "Шинэчлэгдсэн имэйл хаяг нь одоогийн имэйл хаягтай ижил байна", + "same_profile_name": "Шинэчлэгдсэн профайлын нэр нь одоогийн профайлын нэртэй ижил байна", + "script_fail": "Урьдчилсан хүсэлтийн скриптийг ажиллуулж чадсангүй", + "something_went_wrong": "Ямар нэг алдаа гарлаа", + "subscription_error": "Энэ сэдвийг захиалж чадсангүй: {error}", + "post_request_script_fail": "Хүсэлтийн дараах скриптийг ажиллуулж чадсангүй", + "reading_files": "Нэг буюу хэд хэдэн файлыг унших явцад алдаа гарлаа.", + "fetching_access_tokens_list": "Токенуудын жагсаалтыг татах явцад алдаа гарлаа", + "generate_access_token": "Хандалтын тэмдгийг үүсгэх явцад алдаа гарлаа", + "delete_access_token": "Хандалтын тэмдгийг устгах явцад алдаа гарлаа", + "extension_not_found": "Өргөтгөл олдсонгүй", + "invalid_request": "Хүсэлтийн өгөгдөл буруу байна" + }, + "export": { + "as_json": "JSON хэлбэрээр экспортлох", + "create_secret_gist": "Нууц гол санааг үүсгэ", + "create_secret_gist_tooltip_text": "Нууц гол болгон экспортлох", + "failed": "Экспортлох явцад алдаа гарлаа", + "secret_gist_success": "Нууц Gist болгон амжилттай экспортоллоо", + "require_github": "GitHub-ээр нэвтэрч нууц гол санааг үүсгэнэ үү", + "title": "Экспорт", + "success": "Амжилттай экспортоллоо" + }, + "file_upload": { + "choose_file": "Файл сонгоно уу", + "max_size_format": "Хамгийн ихдээ 5MB. JPEG, PNG, GIF, WebP дэмждэг", + "profile_photo_updated": "Профайл зургийг амжилттай шинэчилсэн", + "profile_photo_removed": "Профайлын зургийг амжилттай устгалаа", + "org_logo_updated": "Байгууллагын лого амжилттай шинэчлэгдсэн", + "error_size_limit": "Файлын хэмжээ 5 МБ-аас бага байх ёстой", + "error_invalid_format": "Файл нь зураг байх ёстой (JPEG, PNG, GIF, эсвэл WebP)", + "error_invalid_upload_type": "Буруу байршуулах төрөл", + "error_invalid_org_id": "Байгууллагын ID формат буруу байна", + "error_upload_failed": "Байршуулж чадсангүй. Дахин оролдоно уу", + "error_network_failed": "Сүлжээний алдаа. Холболтоо шалгана уу", + "error_timeout": "30 секундын дараа байршуулах хугацаа дууссан. Дахин оролдоно уу", + "error_missing_backend_url": "Backend URL-г тохируулаагүй байна. Орчны тохиргоондоо VITE_BACKEND_API_URL орчны хувьсагчийг тохируулна уу", + "error_invalid_backend_url": "Буруу URL тохиргоо" + }, + "filename": { + "cookie_key_value_pairs": "Күүки", + "codegen": "{request_name}- код", + "graphql_response": "GraphQL-Хариу", + "lens": "{request_name}- хариулт", + "realtime_response": "Бодит цагийн хариу", + "response_interface": "Хариулт-Интерфэйс" + }, + "filter": { + "all": "Бүгд", + "none": "Байхгүй", + "starred": "Одтой" + }, + "folder": { + "created": "Хавтас үүсгэсэн", + "edit": "Хавтас засах", + "invalid_name": "Фолдерт нэр өгнө үү", + "name_length_insufficient": "Хавтасны нэр дор хаяж 3 тэмдэгт байх ёстой", + "new": "Шинэ хавтас", + "run": "Фолдер ажиллуулах", + "renamed": "Хавтасны нэрийг өөрчилсөн", + "sorted": "Фолдерыг эрэмбэлсэн" + }, + "graphql": { + "arguments": "Аргументууд", + "connection_switch_confirm": "Та хамгийн сүүлийн үеийн GraphQL төгсгөлтэй холбогдохыг хүсэж байна уу?", + "connection_error_http": "Сүлжээний алдааны улмаас GraphQL схемийг татаж авч чадсангүй.", + "connection_switch_new_url": "Таб руу шилжих нь таныг идэвхтэй GraphQL холболтоос салгах болно. Шинэ холболтын URL байна", + "connection_switch_url": "Та GraphQL-ийн төгсгөлийн цэгт холбогдсон байна, холболтын URL нь байна", + "deprecated": "Хаагдсан", + "fields": "Талбайнууд", + "mutation": "Мутаци", + "mutations": "Мутаци", + "schema": "Схем", + "show_depricated_values": "Хуучин утгыг харуулах", + "subscription": "Захиалга", + "subscriptions": "Захиалга", + "switch_connection": "Холболтыг солих", + "url_placeholder": "GraphQL төгсгөлийн URL-г оруулна уу", + "query": "Асуулга" + }, + "graphql_collections": { + "title": "GraphQL цуглуулгууд" + }, + "group": { + "time": "Цаг хугацаа", + "url": "URL" + }, + "header": { + "install_pwa": "Програмыг суулгана уу", + "login": "Нэвтрэх", + "save_workspace": "Миний ажлын талбарыг хадгалах" + }, + "helpers": { + "authorization": "Таныг хүсэлт илгээх үед зөвшөөрлийн гарчиг автоматаар үүсгэгдэнэ.", + "collection_properties_authorization": "Энэхүү зөвшөөрлийг энэ цуглуулгын хүсэлт бүрт тохируулна.", + "collection_properties_header": "Энэ цуглуулгын хүсэлт бүрт энэ толгойг тохируулах болно.", + "collection_properties_scripts": "Эдгээр скриптүүд нь энэ цуглуулгын хүсэлт бүрт ажиллах болно. Урьдчилсан хүсэлтийн скриптүүд хүсэлтийн өмнө, тестийн скриптүүд хариултын дараа ажиллана.", + "generate_documentation_first": "Эхлээд баримт бичгийг бүрдүүлэх", + "network_fail": "API төгсгөлийн цэгт хүрч чадсангүй. Сүлжээний холболтоо шалгах эсвэл өөр Interceptor сонгоод дахин оролдоно уу.", + "offline": "Та офлайнаар Hoppscotch ашиглаж байна. Ажлын байрны тохиргоонд тулгуурлан таныг онлайн байх үед шинэчлэлтүүд синк хийнэ.", + "offline_short": "Та офлайнаар Hoppscotch ашиглаж байна.", + "post_request_tests": "Хүсэлтийн дараах скриптүүд нь JavaScript дээр бичигдсэн бөгөөд хариуг хүлээн авсны дараа ажиллуулдаг.", + "pre_request_script": "Урьдчилсан хүсэлтийн скриптүүд нь JavaScript дээр бичигдсэн бөгөөд хүсэлтийг илгээхээс өмнө ажиллуулдаг.", + "script_fail": "Урьдчилсан хүсэлтийн скриптэд алдаа гарсан бололтой. Доорх алдааг шалгаад скриптийг тохируулна уу.", + "post_request_script_fail": "Хүсэлтийн дараах скриптэд алдаа гарсан бололтой. Алдааг засаад дахин туршилт явуулна уу", + "post_request_script": "Дибаг хийхийг автоматжуулахын тулд хүсэлтийн дараах скрипт бичнэ үү." + }, + "hide": { + "collection": "Цуглуулгын самбарыг буулгах", + "more": "Илүү ихийг нуу", + "preview": "Урьдчилан харахыг нуух", + "sidebar": "Хажуугийн самбарыг буулгах", + "password": "Нууц үгээ нуух" + }, + "import": { + "collections": "Цуглуулга импортлох", + "curl": "cURL импортлох", + "environments_from_gist": "Gist-ээс импортлох", + "environments_from_gist_description": "Gist-ээс Hoppscotch орчныг импортлох", + "failed": "Импортлох явцад гарсан алдаа: форматыг танихгүй байна", + "from_file": "Файлаас импортлох", + "from_gist": "Gist-ээс импортлох", + "from_gist_description": "Gist URL-аас импортлох", + "from_gist_import_summary": "Хоппскочны бүх функцийг импортоор оруулсан болно.", + "from_hoppscotch_importer_summary": "Хоппскочны бүх функцийг импортоор оруулсан болно.", + "from_insomnia": "Нойргүйдэлээс импортлох", + "from_insomnia_description": "Insomnia цуглуулгаас импортлох", + "from_insomnia_import_summary": "Цуглуулга болон хүсэлтийг импортлох болно.", + "from_json": "Хоппскочоос импортлох", + "from_json_description": "Hoppscotch цуглуулгын файлаас импортлох", + "from_my_collections": "Хувийн цуглуулгаас импортлох", + "from_my_collections_description": "Хувийн цуглуулгын файлаас импортлох", + "from_all_collections": "Өөр ажлын талбараас импорт хийх", + "from_all_collections_description": "Өөр ажлын талбараас аливаа цуглуулгыг одоогийн ажлын талбарт импортлох", + "from_openapi": "OpenAPI-аас импортлох", + "from_openapi_description": "OpenAPI тодорхойлолтын файлаас импорт хийх (YML/JSON)", + "from_openapi_import_summary": "Цуглуулга (шошгуудаас бий болно), Хүсэлт болон хариултын жишээг импортлох болно.", + "from_postman": "Шууданчаас импортлох", + "from_postman_description": "Шуудангийн цуглуулгаас импортлох", + "from_postman_import_summary": "Цуглуулга, хүсэлт, хариултын жишээг импортлох болно.", + "import_scripts": "Скриптүүдийг импортлох", + "import_scripts_description": "Postman Collection v2.0/v2.1-ийг дэмждэг.", + "from_url": "URL-аас импортлох", + "gist_url": "Gist URL-г оруулна уу", + "from_har": "HAR-аас импортлох", + "from_har_description": "HAR файлаас импортлох", + "from_har_import_summary": "Хүсэлтийг өгөгдмөл цуглуулгад импортлох болно.", + "gql_collections_from_gist_description": "Gist-ээс GraphQL цуглуулгуудыг импортлох", + "hoppscotch_environment": "Хоппскотийн орчин", + "hoppscotch_environment_description": "Hoppscotch Environment JSON файлыг импортлох", + "import_from_url_invalid_fetch": "URL-аас өгөгдөл авч чадсангүй", + "import_from_url_invalid_file_format": "Цуглуулга импортлох үед алдаа гарлаа", + "import_from_url_invalid_type": "Дэмжигдээгүй төрөл. хүлээн зөвшөөрөгдсөн үнэ цэнэ нь \"хоппскоч\", \"openapi\", \"шуудан зөөгч\", \"нойргүйдэл\"", + "import_from_url_success": "Импортолсон цуглуулгууд", + "insomnia_environment_description": "JSON/YAML файлаас нойргүйдлийн орчныг импортлох", + "json_description": "Hoppscotch Collections JSON файлаас цуглуулгуудыг импортлох", + "postman_environment": "Шуудангийн орчин", + "postman_environment_description": "JSON файлаас шуудангийн орчныг импортлох", + "title": "Импорт", + "file_size_limit_exceeded_warning_multiple_files": "Сонгосон файлууд нь санал болгож буй {sizeLimit}MB хязгаараас хэтэрсэн байна. Зөвхөн эхний сонгосон {files}-г импортлох болно", + "file_size_limit_exceeded_warning_single_file": "Одоогоор сонгосон файл нь санал болгож буй {sizeLimit}MB хязгаараас хэтэрсэн байна. Өөр файл сонгоно уу.", + "success": "Амжилттай оруулж ирлээ", + "import_summary_collections_title": "Цуглуулга", + "import_summary_requests_title": "Хүсэлтүүд", + "import_summary_responses_title": "Хариултууд", + "import_summary_pre_request_scripts_title": "Скриптүүдийг урьдчилан хүсэх", + "import_summary_post_request_scripts_title": "Хүсэлтийн скриптүүдийг нийтлэх", + "import_summary_not_supported_by_hoppscotch_import": "Бид яг одоо энэ эх сурвалжаас {featureLabel} импортлохыг дэмжихгүй байна.", + "import_summary_script_found": "скрипт олдсон боловч импортлоогүй", + "import_summary_scripts_found": "скриптүүд олдсон боловч импортлогдоогүй", + "import_summary_enable_experimental_sandbox": "Postman скриптүүдийг импортлохын тулд тохиргооноос \"Туршилтын скриптийн хамгаалагдсан хязгаарлагдмал орчин\"-г идэвхжүүлнэ үү. Тайлбар: Энэ функц нь туршилтын шинж чанартай.", + "cors_error_modal": { + "title": "CORS алдаа илэрсэн", + "description": "Серверээс тавьсан CORS (Гарал үүслийн эх сурвалжийг хуваалцах) хязгаарлалтын улмаас импорт амжилтгүй болсон.", + "explanation": "Энэ нь вэб хуудсууд өөр өөр домэйнд хүсэлт гаргахаас сэргийлдэг хамгаалалтын функц юм. Та энэ хязгаарлалтыг давахын тулд манай прокси үйлчилгээг ашиглан дахин оролдох боломжтой.", + "url_label": "Оролдсон URL", + "retry_with_proxy": "Прокси ашиглан дахин оролдоно уу" + } + }, + "instances": { + "switch": "Hoppscotch Instance солих", + "enter_server_url": "Өөрөө байршуулсан инстанттай холбогдоно уу", + "already_connected": "Та энэ жишээнд аль хэдийн холбогдсон байна", + "recent_connections": "Сүүлийн үеийн холболтууд", + "add_instance": "Жишээ нэмнэ үү", + "add_new": "Шинэ жишээ нэмнэ үү", + "confirm_remove": "Устгасныг баталгаажуулна уу", + "remove_warning": "Та энэ жишээг устгахдаа итгэлтэй байна уу?", + "clear_cached_bundles": "Кэш хийсэн багцуудыг арилгах", + "opening_add_modal": "Жишээ нэмэх харилцах цонхыг нээж байна", + "closed_add_modal": "Жишээ нэмэх харилцах цонх хаагдсан", + "cancelled_removal": "Нэгэн жишээг устгахыг цуцалсан", + "connection_cancelled": "Урьдчилан холболтын баталгаажуулалтаар холболтыг цуцалсан", + "post_connect_completed": "Холболтын дараах тохируулга дууссан", + "connecting": "Жишээ рүү холбогдож байна...", + "confirm_removal": "Жишээ устгасныг баталгаажуулна уу", + "removal_cancelled": "Устгах өмнөх баталгаажуулалтаар жишээ устгалыг цуцалсан", + "post_remove_completed": "Устгасны дараах цэвэрлэгээ дууссан", + "removing": "Жишээ устгаж байна...", + "clearing_cache": "Кэшийг цэвэрлэж байна...", + "initialized": "Инстанц шилжүүлэгчийг эхлүүлсэн", + "connecting_state": "Холболт үүсгэж байна...", + "connected_state": "Жишээнд амжилттай холбогдсон", + "disconnected_state": "Жишээнээс салсан", + "stream_error": "Холболтын төлөвийн хяналт амжилтгүй боллоо", + "recent_instances_error": "Сүүлийн тохиолдлуудыг ачаалж чадсангүй", + "instance_changed": "Жишээ рүү шилжсэн", + "current_instance_error": "Одоогийн жишээг хянаж чадсангүй", + "not_available": "Жишээ солих боломжгүй", + "cleanup_completed": "Инстанс шилжүүлэгчийн цэвэрлэгээ дууссан", + "self_hosted": "Өөрөө байршуулсан тохиолдлууд" + }, + "inspections": { + "description": "Боломжит алдааг шалгана уу", + "environment": { + "add_environment": "Байгаль орчинд нэмэх", + "add_environment_value": "Үнэ цэнэ нэмэх", + "empty_value": "'{variable}' хувьсагчийн орчны утга хоосон байна", + "not_found": "'{environment}' орчны хувьсагч олдсонгүй." + }, + "header": { + "cookie": "Хөтөч нь Hoppscotch-д күүки толгойг тохируулахыг зөвшөөрдөггүй. Оронд нь Зөвшөөрлийн толгой хэсгийг ашиглана уу. Гэсэн хэдий ч манай Hoppscotch Desktop App одоо ажиллаж байгаа бөгөөд күүки дэмждэг." + }, + "response": { + "401_error": "Баталгаажуулах үнэмлэхээ шалгана уу.", + "404_error": "Хүсэлтийн URL болон аргын төрлийг шалгана уу.", + "cors_error": "Эх сурвалж хоорондын нөөц хуваалцах тохиргоогоо шалгана уу.", + "default_error": "Хүсэлтээ шалгана уу.", + "network_error": "Сүлжээний холболтоо шалгана уу." + }, + "title": "Байцаагч", + "url": { + "extension_not_installed": "Өргөтгөлийг суулгаагүй байна.", + "extension_unknown_origin": "Та API төгсгөлийн цэгийн гарал үүслийг Hoppscotch Browser Extension жагсаалтад нэмсэн эсэхээ шалгаарай.", + "extention_enable_action": "Хөтөчийн өргөтгөлийг идэвхжүүлнэ үү", + "extention_not_enabled": "Өргөтгөлийг идэвхжүүлээгүй.", + "localaccess_unsupported": "Одоогийн саад тотгор нь орон нутгийн хандалтыг дэмждэггүй тул Agent, Extension interceptors эсвэл Desktop App ашиглана уу." + }, + "auth": { + "digest": "Digest Authorization-ийг ашиглах үед вэб апп дээрх агент таслан зогсоох хэрэгсэл эсвэл ширээний программ дээрх Native interceptor ашиглахыг зөвлөж байна.", + "hawk": "Hawk Authorization-г ашиглах үед вэб програм дээрх агент таслан зогсоох хэрэгсэл эсвэл Ширээний програм дээрх Native interceptor ашиглахыг зөвлөж байна." + }, + "body": { + "binary": "Одоогийн таслагчаар хоёртын өгөгдөл илгээхийг хараахан дэмжээгүй байна." + }, + "scripting_interceptor": { + "pre_request": "урьдчилсан хүсэлтийн скрипт", + "post_request": "хүсэлтийн дараах скрипт", + "both_scripts": "хүсэлтийн өмнөх болон дараах скриптүүд", + "unsupported_interceptor": "Таны {scriptType} нь {apiUsed} ашигладаг. Скриптийг найдвартай гүйцэтгэхийн тулд Agent interceptor (вэб програм) эсвэл Native interceptor (Desktop app) руу шилжинэ үү.{interceptor} саатуулагч нь скрипт бичих хүсэлтийг хязгаарлагдмал дэмждэг бөгөөд санаснаар ажиллахгүй байж магадгүй.", + "same_origin_csrf_warning": "Аюулгүй байдлын сэрэмжлүүлэг: Таны {scriptType} нь {apiUsed} ашиглан ижил төрлийн хүсэлт гаргадаг. Энэхүү платформ нь күүки дээр суурилсан баталгаажуулалтыг ашигладаг тул эдгээр хүсэлтүүд нь таны сесс күүкиг автоматаар агуулж, хортой скриптүүдэд зөвшөөрөлгүй үйлдэл хийхийг зөвшөөрдөг. Ижил гарал үүслийн хүсэлтүүдэд Agent interceptor-г ашиглах эсвэл зөвхөн итгэдэг скриптүүдээ ажиллуул." + } + }, + "interceptor": { + "native": { + "name": "Төрөлх", + "settings_title": "Төрөлх" + }, + "agent": { + "name": "Агент", + "settings_title": "Агент" + }, + "proxy": { + "name": "Прокси", + "settings_title": "Прокси" + }, + "browser": { + "name": "Хөтөч", + "settings_title": "Хөтөч" + }, + "extension": { + "name": "Өргөтгөл", + "settings_title": "Өргөтгөл" + } + }, + "layout": { + "collapse_collection": "Цуглуулгыг задлах эсвэл өргөжүүлэх", + "collapse_sidebar": "Хажуугийн самбарыг буулгах эсвэл өргөжүүлэх", + "column": "Босоо байрлал", + "name": "Зохион байгуулалт", + "row": "Хэвтээ байрлал" + }, + "modal": { + "close_unsaved_tab": "Танд хадгалагдаагүй өөрчлөлтүүд байна", + "collections": "Цуглуулга", + "confirm": "Баталгаажуулах", + "customize_request": "Хүсэлтийг тохируулах", + "edit_request": "Хүсэлтийг засах", + "edit_response": "Хариултыг засах", + "import_export": "Импорт / Экспорт", + "response_name": "Хариултын нэр", + "share_request": "Хүсэлт хуваалцах" + }, + "mqtt": { + "already_subscribed": "Та энэ сэдэвт аль хэдийн бүртгүүлсэн байна.", + "clean_session": "Цэвэр сесс", + "clear_input": "Оролтоо арилгах", + "clear_input_on_send": "Илгээх үед оруулгыг арилгах", + "client_id": "Үйлчлүүлэгчийн ID", + "color": "Өнгө сонгоно уу", + "communication": "Харилцаа холбоо", + "connection_config": "Холболтын тохиргоо", + "connection_not_authorized": "Энэ MQTT холболт нь ямар ч баталгаажуулалт ашигладаггүй.", + "invalid_topic": "Захиалгын сэдвийг оруулна уу", + "keep_alive": "Амьд байлга", + "log": "Бүртгэл", + "lw_message": "Эцсийн хүслийн зурвас", + "lw_qos": "Сүүлчийн хүсэл эрмэлзэлийн үйлчилгээний чанар", + "lw_retain": "Сүүлд хадгалагдах болно", + "lw_topic": "Эцсийн хүсэл сэдэв", + "message": "Мессеж", + "new": "Шинэ захиалга", + "not_connected": "Эхлээд MQTT холболтыг эхлүүлнэ үү.", + "publish": "Нийтлэх", + "qos": "QS", + "ssl": "SSL", + "subscribe": "Бүртгүүлэх", + "topic": "Сэдэв", + "topic_name": "Сэдвийн нэр", + "topic_title": "Сэдвийг нийтлэх / захиалах", + "unsubscribe": "Бүртгэлээ цуцлах", + "url": "URL" + }, + "navigation": { + "admin_dashboard": "Хяналтын самбар", + "doc": "Докс", + "graphql": "GraphQL", + "profile": "Профайл", + "realtime": "Бодит цаг", + "rest": "АМРАХ", + "mock_servers": "Хуурамч серверүүд", + "settings": "Тохиргоо", + "goto_app": "Програм руу очно уу", + "authentication": "Баталгаажуулалт" + }, + "mock_server": { + "confirm_delete_log": "Та энэ бүртгэлийг устгахдаа итгэлтэй байна уу?", + "create_mock_server": "Хуурамч серверийг тохируулах", + "mock_server_configuration": "Хуурамч серверийн тохиргоо", + "mock_server_name": "Хуурамч серверийн нэр", + "mock_server_name_placeholder": "Хуурамч серверийн нэрийг оруулна уу", + "base_url": "Үндсэн URL", + "start_server": "Серверийг эхлүүлэх", + "stop_server": "Серверийг зогсоох", + "mock_server_created": "Хуурамч серверийг амжилттай үүсгэсэн", + "mock_server_started": "Хуурамч сервер амжилттай эхэлсэн", + "mock_server_stopped": "Хуурамч сервер амжилттай зогслоо", + "active": "Хуурамч сервер идэвхтэй байна", + "inactive": "Хуурамч сервер идэвхгүй байна", + "edit_mock_server": "Хуурамч серверийг засах", + "path_based_url": "Зам дээр суурилсан URL", + "subdomain_based_url": "Дэд домайн дээр суурилсан URL", + "mock_server_updated": "Хуурамч серверийг амжилттай шинэчилсэн", + "no_collection": "Цуглуулга байхгүй", + "collection_deleted": "холбоотой цуглуулгыг устгасан.", + "private_access_hint": "Хувийн хуурамч серверүүдийн хувьд 'x-api-key' толгойг Хувийн хандалтын тэмдэгтээ оруулна уу (профайлаасаа нэгийг үүсгэнэ үү).", + "private_access_instruction": "Энэхүү хувийн хуурамч серверт хандахын тулд \"x-api-key\" толгой хэсгийг хувийн хандалтын токендоо оруулна уу.", + "create_token_here": "Энд үүсгэнэ үү", + "status": "Статус", + "server_running": "Сервер ажиллаж байна", + "server_stopped": "Сервер зогссон байна", + "delay_ms": "Хариу өгөх саатал (мс)", + "delay_placeholder": "Сааталыг миллисекундээр оруулна уу", + "delay_description": "Хуурамч хариулт өгөхийн тулд зохиомол саатал нэмнэ үү", + "public_access": "Нийтийн хүртээмж", + "public": "Олон нийтийн", + "private": "Хувийн", + "public_description": "URL-тай хэн ч энэ хуурамч серверт хандах боломжтой", + "private_description": "Зөвхөн баталгаажсан хэрэглэгчид энэ хуурамч серверт хандах боломжтой", + "select_collection": "Цуглуулга сонгоно уу", + "select_collection_error": "Цуглуулга сонгоно уу", + "invalid_collection_error": "Цуглуулгын хуурамч сервер үүсгэж чадсангүй.", + "url_copied": "URL-г санах ой руу хуулсан", + "make_public": "Олон нийтэд нээлттэй болгох", + "view_logs": "Бүртгэлүүдийг үзэх", + "logs_title": "Хуурамч серверийн бүртгэл", + "no_logs": "Лог байхгүй байна", + "request_headers": "Хүсэлтийн толгой", + "request_body": "Хүсэлтийн байгууллага", + "response_status": "Хариу үйлдлийн төлөв", + "response_headers": "Хариултын толгой", + "response_body": "Хариу өгөх байгууллага", + "log_deleted": "Бүртгэлийг амжилттай устгалаа", + "description": "Хуурамч серверүүд нь цуглуулгын жишээ хариултууд дээр үндэслэн API хариултуудыг дуурайлган хийх боломжийг олгодог.", + "set_in_environment": "Хүрээлэн буй орчинд тохируулна", + "set_in_environment_hint": "Хуурамч серверийн URL нь цуглуулгын орчинд автоматаар \"mockUrl\" хувьсагчаар нэмэгдэх болно", + "environment_variable_added": "Хүрээлэн буй орчинд хуурамч URL нэмсэн", + "environment_variable_updated": "Хуурамч URL орчинд шинэчлэгдсэн", + "environment_created_with_variable": "Хуурамч URL-аар үүсгэсэн орчин", + "add_example_request": "Жишээ хүсэлт нэмнэ үү", + "add_example_request_hint": "Цуглуулга нь хуурамч серверийг хэрхэн ашиглахыг харуулсан жишээ хүсэлтээр үүсгэгдэнэ", + "create_example_collection": "Жишээ цуглуулга үүсгэх", + "create_example_collection_hint": "Амьтны дэлгүүрийн жишээ цуглуулгыг жишээ хүсэлтээр үүсгэх (GET, POST, PUT, DELETE)", + "creating_example_collection": "Жишээ цуглуулгыг үүсгэж байна...", + "failed_to_create_collection": "Жишээ цуглуулгыг үүсгэж чадсангүй", + "enable_example_collection_hint": "Шинэ цуглуулгын горимд \"Жишээ цуглуулга үүсгэх\"-г идэвхжүүлнэ үү", + "new_collection_name_hint": "Цуглуулга нь таны хуурамч сервертэй ижил нэрээр үүсгэгдэх болно", + "existing_collection": "Одоо байгаа цуглуулга", + "new_collection": "Шинэ цуглуулга" + }, + "preRequest": { + "javascript_code": "JavaScript код", + "learn": "Баримт бичгийг уншина уу", + "script": "Урьдчилсан хүсэлтийн скрипт", + "snippets": "Хэсэг хэсгүүд" + }, + "profile": { + "app_settings": "Програмын тохиргоо", + "default_hopp_displayname": "Нэргүй хэрэглэгч", + "editor": "Редактор", + "editor_description": "Засварлагчид хүсэлт нэмэх, засах, устгах боломжтой.", + "email_verification_mail": "Баталгаажуулах имэйлийг таны имэйл хаяг руу илгээсэн. Холбоос дээр дарж имэйл хаягаа баталгаажуулна уу.", + "no_permission": "Танд энэ үйлдлийг хийх зөвшөөрөл байхгүй байна.", + "owner": "Эзэмшигч", + "owner_description": "Эзэмшигчид хүсэлт, цуглуулга, ажлын талбарын гишүүдийг нэмэх, засах, устгах боломжтой.", + "roles": "Дүрүүд", + "roles_description": "Хуваалцсан цуглуулгад хандах хандалтыг хянахын тулд дүрүүдийг ашигладаг.", + "updated": "Профайл шинэчлэгдсэн", + "viewer": "Үзэгч", + "viewer_description": "Үзэгчид зөвхөн хүсэлтийг үзэж, ашиглах боломжтой.", + "verified_email_sent": "Баталгаажуулах имэйлийг таны имэйл хаяг руу илгээсэн. Имэйл хаягаа баталгаажуулсны дараа хуудсыг дахин сэргээнэ үү. Хэрэв энэ имэйл өөр бүртгэлтэй холбоогүй бол та имэйл хүлээн авах болно." + }, + "remove": { + "star": "Од арилгах" + }, + "request": { + "added": "Хүсэлт нэмсэн", + "add": "Хүсэлт нэмэх", + "authorization": "Зөвшөөрөл", + "body": "Хүсэлтийн байгууллага", + "choose_language": "Хэл сонгоно уу", + "content_type": "Агуулгын төрөл", + "content_type_titles": { + "others": "Бусад", + "structured": "Бүтэцтэй", + "text": "Текст", + "binary": "Хоёртын" + }, + "show_content_type": "Агуулгын төрлийг харуулах", + "different_collection": "Өөр цуглуулгуудын хүсэлтийг дахин эрэмбэлэх боломжгүй", + "duplicated": "Хүсэлт давхардсан", + "duration": "Үргэлжлэх хугацаа", + "enter_curl": "cURL командыг оруулна уу", + "generate_code": "Код үүсгэх", + "generated_code": "Үүсгэсэн код", + "go_to_authorization_tab": "Зөвшөөрлийн таб руу очно уу", + "go_to_body_tab": "Биеийн таб руу очно уу", + "header_list": "Толгойн жагсаалт", + "invalid_name": "Хүсэлтийн нэрийг оруулна уу", + "method": "Арга", + "moved": "Хүсэлтийг шилжүүлсэн", + "name": "Нэрийг хүсэх", + "new": "Шинэ хүсэлт", + "order_changed": "Хүсэлтийн захиалгыг шинэчилсэн", + "override": "Дарж бичих", + "override_help": "Толгой хэсэгтагуулгын төрлийгтохируулах", + "overriden": "Дарсан", + "parameter_list": "Асуулгын параметрүүд", + "parameters": "Параметрүүд", + "path": "Зам", + "payload": "Ачаалал", + "query": "Асуулга", + "raw_body": "Түүхий хүсэлтийн байгууллага", + "rename": "Хүсэлтийн нэрийг өөрчлөх", + "renamed": "Хүсэлтийн нэрийг өөрчилсөн", + "request_variables": "Хувьсагчийг хүсэх", + "response_name_exists": "Хариултын нэр аль хэдийн байна", + "run": "Гүй", + "save": "Хадгалах", + "save_as": "Хадгалах", + "saved": "Хүсэлтийг хадгалсан", + "share": "Хуваалцах", + "share_description": "Hoppscotch-ийг найзуудтайгаа хуваалцаарай", + "share_request": "Хүсэлт хуваалцах", + "stop": "Зогс", + "title": "Хүсэлт", + "type": "Хүсэлтийн төрөл", + "url": "URL", + "url_placeholder": "URL оруулна уу эсвэл cURL командыг буулгана уу", + "variables": "Хувьсагч", + "view_my_links": "Миний холбоосуудыг үзэх", + "generate_name_error": "Хүсэлтийн нэрийг үүсгэж чадсангүй." + }, + "response": { + "audio": "Аудио", + "body": "Хариу өгөх байгууллага", + "duplicated": "Хариултыг давхардсан", + "duplicate_name_error": "Ижил нэртэй хариулт аль хэдийн байна", + "filter_response_body": "JSON хариултын хэсгийг шүүх (jq синтакс ашигладаг)", + "headers": "Гарчиг", + "request_headers": "Хүсэлтийн толгой", + "html": "HTML", + "image": "Зураг", + "json": "JSON", + "pdf": "PDF", + "please_save_request": "Жишээ үүсгэх хүсэлтийг хадгална уу", + "preview_html": "HTML-г урьдчилан үзэх", + "raw": "Түүхий", + "renamed": "Хариултын нэрийг өөрчилсөн", + "same_name_inspector_warning": "Энэ хүсэлтийн хариуны нэр аль хэдийн байгаа бөгөөд хадгалсан бол одоо байгаа хариултыг дарж бичих болно", + "size": "Хэмжээ", + "status": "Статус", + "time": "Цаг хугацаа", + "title": "Хариулт", + "video": "Видео", + "waiting_for_connection": "холболт хүлээж байна", + "xml": "XML", + "generate_data_schema": "Өгөгдлийн схем үүсгэх", + "data_schema": "Өгөгдлийн схем", + "saved": "Хариултыг хадгалсан", + "invalid_name": "Хариуд нь нэр өгнө үү" + }, + "script": { + "inheriting": "-аас скриптүүдийг өвлөн авч байна", + "inheriting_from_count": "{count} цуглуулгаас өвлөн авч байна |{count} цуглуулгаас өвлөн авч байна", + "inherited_scripts": "Өвлөгдсөн скриптүүд", + "view_inherited": "Өвлөгдсөн скриптүүдийг үзэх" + }, + "settings": { + "accent_color": "Өргөлтийн өнгө", + "account": "Данс", + "account_deleted": "Таны бүртгэл устсан", + "account_description": "Бүртгэлийнхээ тохиргоог өөрчил.", + "account_email_description": "Таны үндсэн имэйл хаяг.", + "account_name_description": "Энэ бол таны дэлгэцийн нэр юм.", + "additional": "Нэмэлт тохиргоо", + "agent_not_running": "Hoppscotch Agent илрээгүй. Агент ажиллаж байгаа эсэхийг шалгана уу.", + "agent_not_running_short": "Агентын статусыг шалгана уу.", + "agent_running": "Hoppscotch Agent шууд нэвтрүүлж байна.", + "agent_running_short": "Hoppscotch Agent шууд нэвтрүүлж байна.", + "agent_discard_registration": "Агентын бүртгэлээс татгалзах", + "agent_registered": "Агент бүртгэгдсэн", + "agent_registration_successful": "Агентыг амжилттай бүртгүүлэв", + "agent_registration_fetch_failed": "Агентын бүртгэлийн мэдээллийг авч чадсангүй. Агентыг дахин бүртгүүлнэ үү.", + "agent_registration_already_in_progress": "Агентын бүртгэл аль хэдийн явагдаж байна. Одоогийн бүртгэлийг дуусгах эсвэл цуцлаад дахин оролдоно уу.", + "auto_encode_mode": "Автомат", + "auto_encode_mode_tooltip": "Зөвхөн зарим тусгай тэмдэгтүүд байгаа тохиолдолд хүсэлтийн параметрүүдийг кодчил", + "background": "Суурь", + "black_mode": "Хар", + "choose_language": "Хэл сонгоно уу", + "dark_mode": "Харанхуй", + "delete_account": "Бүртгэл устгах", + "delete_account_description": "Бүртгэлээ устгасны дараа таны бүх өгөгдөл бүрмөсөн устах болно. Энэ үйлдлийг буцаах боломжгүй.", + "desktop": "Ширээний компьютер", + "desktop_description": "Hoppscotch ширээний програмын зан байдал, гарны ашиглалтыг шинэчилнэ үү.", + "desktop_keyboard": "Гар", + "desktop_keyboard_strategy_label": "Товчлолыг бичсэн үсэг эсвэл физик байрлалаар нь тааруулна уу", + "desktop_keyboard_strategy_description": "QWERTY бус загвар дээр ижил үсэг өөр өөр физик түлхүүрээс ирж болно. Өгөгдмөл нь ихэнх байршилд ажилладаг; Хэрэв товчлол таны санаснаар ажиллахгүй бол сонголтуудыг сэлгэнэ үү.", + "desktop_keyboard_strategy_hybrid": "Ухаалаг (санал болгосон)", + "desktop_keyboard_strategy_hybrid_description": "Латин үсгээр бичсэн үсгийг ашиглах; Латин бус байршлын (Кирилл, CJK) физик түлхүүрийн байрлал руу буцах.", + "desktop_keyboard_strategy_key": "Бичсэн үсэг", + "desktop_keyboard_strategy_key_description": "Бичсэн үсгийг үргэлж ашигла. Хэрэв товчлолууд таны зохион байгуулалтад санаснаар ажиллахгүй байвал үүнийг сонгоно уу.", + "desktop_keyboard_strategy_code": "Физик гол байрлал", + "desktop_keyboard_strategy_code_description": "АНУ-QWERTY биеийн байрлалыг үргэлж ашиглаарай. Хэрэв та латин бус загвар дээр QWERTY булчингийн санах ойтой бол үүнийг сонгоорой.", + "desktop_updates": "Шинэчлэлтүүд", + "disable_encode_mode_tooltip": "Хүсэлт дэх параметрүүдийг хэзээ ч кодлох хэрэггүй", + "disable_update_checks": "Автомат шинэчлэлтийн шалгалтыг идэвхгүй болгох", + "disable_update_checks_description": "Програмыг эхлүүлэх үед шинэчлэлтийн шалгалтыг алгасах. Хүсэлтийг шалгахын тулд дээрх товчлуурыг ашиглана уу.", + "enable_encode_mode_tooltip": "Хүсэлт дэх параметрүүдийг үргэлж кодчил", + "enter_otp": "Агентын кодыг оруулна уу", + "expand_navigation": "Навигацыг өргөжүүлэх", + "experiments": "Туршилтууд", + "experiments_notice": "Энэ бол бидний ажиллаж байгаа туршилтуудын цуглуулга бөгөөд ашигтай, хөгжилтэй, хоёулаа эсвэл аль нь ч биш байж магадгүй юм. Эдгээр нь эцсийнх биш бөгөөд тогтвортой биш байж магадгүй тул хэт хачирхалтай зүйл тохиолдвол сандрах хэрэггүй. Аюултай зүйлийг унтраа. Хошигнолын хажуугаар,", + "extension_ver_not_reported": "Мэдээгүй", + "extension_version": "Өргөтгөлийн хувилбар", + "extensions": "Хөтөчийн өргөтгөл", + "extensions_use_toggle": "Хүсэлт илгээхийн тулд хөтчийн өргөтгөлийг ашиглана уу (хэрэв байгаа бол)", + "follow": "Биднийг дага", + "general": "Генерал", + "general_description": "Програмд ашигласан ерөнхий тохиргоо", + "interceptor": "Таслагч", + "interceptor_description": "Аппликешн болон API хоорондын дунд програм.", + "kernel_interceptor": "Таслагч", + "kernel_interceptor_description": "Аппликешн болон API хоорондын дунд програм.", + "language": "Хэл", + "light_mode": "Гэрэл", + "official_proxy_hosting": "Албан ёсны проксиг Hoppscotch зохион байгуулдаг.", + "query_parameters_encoding": "Асуулгын параметрийн кодчилол", + "query_parameters_encoding_description": "Хүсэлт дэх асуулгын параметрүүдийн кодчилолыг тохируулах", + "profile": "Профайл", + "profile_description": "Профайлынхаа дэлгэрэнгүй мэдээллийг шинэчилнэ үү", + "profile_email": "Имэйл хаяг", + "profile_name": "Профайлын нэр", + "profile_photo": "Профайл зураг", + "proxy": "Прокси", + "proxy_url": "Прокси URL", + "proxy_use_toggle": "Хүсэлт илгээхийн тулд прокси дунд програмыг ашиглана уу", + "read_the": "-г уншина уу", + "register_agent": "Агент бүртгүүлэх", + "reset_default": "Өгөгдмөл прокси ашиглах", + "short_codes": "Богино кодууд", + "short_codes_description": "Таны үүсгэсэн богино кодууд.", + "sidebar_on_left": "Зүүн талд байгаа хажуугийн самбар", + "ai_experiments": "AI туршилтууд", + "ai_request_naming_style": "Хүсэлтийн нэрийн хэв маяг", + "ai_request_naming_style_descriptive_with_spaces": "Зайгаар дүрсэлсэн", + "ai_request_naming_style_camel_case": "Тэмээний хайрцаг (тэмээний хайрцаг)", + "ai_request_naming_style_snake_case": "Могойн хайрцаг ( могойн хэрэг )", + "ai_request_naming_style_pascal_case": "Паскаль Кейс (Pascal Case)", + "ai_request_naming_style_custom": "Захиалгат", + "ai_request_naming_style_custom_placeholder": "Өөрийн нэрийн загварын загвараа оруулна уу...", + "experimental_scripting_sandbox": "Туршилтын скриптийн хамгаалагдсан хязгаарлагдмал орчин", + "enable_experimental_mock_servers": "Хуурамч серверүүдийг идэвхжүүл", + "enable_experimental_documentation": "Баримт бичгийг идэвхжүүлэх", + "sync": "Синхрончлох", + "sync_collections": "Цуглуулга", + "sync_description": "Эдгээр тохиргоог үүл рүү синк хийсэн.", + "sync_environments": "Байгаль орчин", + "sync_history": "Түүх", + "history_disabled": "Түүхийг идэвхгүй болгосон. Түүхийг идэвхжүүлэхийн тулд байгууллагын админтай холбогдоно уу", + "system_mode": "Систем", + "telemetry": "Телеметр", + "telemetry_helps_us": "Телеметр нь үйл ажиллагаагаа хувийн болгож, танд хамгийн сайн туршлагыг хүргэхэд бидэнд тусалдаг.", + "theme": "Сэдэв", + "theme_description": "Өөрийн хэрэглээний сэдвийг тохируулна уу.", + "update_check_description": "Гараар шинэ хувилбар байгаа эсэхийг шалгаад суулгана уу. Доорх шилжүүлэгчээс үл хамааран ажиллана.", + "update_check_now": "Шинэчлэлт байгаа эсэхийг шалгана уу", + "update_checking": "Шалгаж байна...", + "update_download_version": "v{version} татаж авах", + "update_downloading": "Татаж авч байна…", + "update_downloading_percent": "Татаж авч байна {percent}%", + "update_installing": "Суулгаж байна...", + "update_restart_now": "Шинэчлэлтийг хэрэгжүүлэхийн тулд дахин эхлүүлнэ үү", + "update_up_to_date": "Шинэчлэгдсэн", + "use_experimental_url_bar": "Орчноо тодруулсан туршилтын URL мөрийг ашиглана уу", + "user": "Хэрэглэгч", + "verified_email": "Баталгаажсан имэйл", + "verify_email": "Имэйлийг баталгаажуулна уу", + "validate_certificates": "SSL/TLS гэрчилгээг баталгаажуулах", + "verify_host": "Хостыг баталгаажуулах", + "verify_peer": "Peer-г баталгаажуулах", + "follow_redirects": "Дахин чиглүүлэлтүүдийг дагах", + "client_certificates": "Үйлчлүүлэгчийн гэрчилгээ", + "certificate_settings": "Сертификат тохиргоо", + "certificate": "Сертификат", + "key": "Хувийн түлхүүр", + "pfx_or_p12": "PFX/PKCS#12", + "password": "Нууц үг", + "select_file": "Файлыг сонгоно уу", + "domain": "Домэйн", + "add_certificate": "Сертификат нэмэх", + "add_cert_file": "Сертификат файл нэмэх", + "add_key_file": "Түлхүүр файл нэмэх", + "add_pfx_file": "PFX файл нэмнэ үү", + "global_defaults": "Глобал өгөгдмөл", + "add_domain_override": "Домэйн хүчингүй болгохыг нэмнэ үү", + "domain_override": "Домэйн хүчингүй болгох", + "manage_domains_overrides": "Домэйн даралтыг удирдах", + "add_domain": "Домэйн нэмэх", + "remove_domain": "Домэйн устгах", + "ca_certificate": "CA гэрчилгээ", + "ca_certificates": "CA гэрчилгээ", + "ca_certificates_support": "Hoppscotch нь нэг буюу хэд хэдэн гэрчилгээ агуулсан .crt, .cer эсвэл .pem файлуудыг дэмждэг.", + "proxy_capabilities": "Hoppscotch Agent болон Desktop App нь NTLM болон Basic Auth дэмжлэг бүхий HTTP/HTTPS/SOCKS прокси дэмждэг.", + "proxy_auth": "Та мөн URL-д хэрэглэгчийн нэр, нууц үг оруулах боломжтой." + }, + "shared_requests": { + "button": "Товчлуур", + "button_info": "Вэбсайт, блог эсвэл README-д зориулж \"Run in Hoppscotch\" товчийг үүсгээрэй.", + "copy_html": "HTML хуулах", + "copy_link": "Холбоосыг хуулах", + "copy_markdown": "Markdown хуулах", + "creating_widget": "Виджет үүсгэж байна", + "customize": "Тохируулах", + "deleted": "Хуваалцсан хүсэлтийг устгасан", + "description": "Виджет сонго, та үүнийг дараа нь өөрчилж, өөрчлөх боломжтой", + "embed": "Оруулсан", + "embed_info": "Өөрийн вэбсайт, блог эсвэл баримт бичигт мини \"Hoppscotch API Playground\" нэмнэ үү.", + "link": "Холбоос", + "link_info": "Хуваалцах боломжтой холбоос үүсгэн үзэх боломжтой интернетэд байгаа хэнтэй ч хуваалцах боломжтой.", + "modified": "Хуваалцсан хүсэлтийг өөрчилсөн", + "not_found": "Хуваалцсан хүсэлт олдсонгүй", + "open_new_tab": "Шинэ таб дээр нээх", + "preview": "Урьдчилан үзэх", + "run_in_hoppscotch": "Хоппскочоор гүйх", + "theme": { + "dark": "Харанхуй", + "light": "Гэрэл", + "system": "Систем", + "title": "Сэдэв" + }, + "action": "Үйлдэл", + "clear_filter": "Шүүлтүүрийг арилгах", + "confirm_request_deletion": "Сонгосон хуваалцсан хүсэлтийг устгахыг баталгаажуулах уу?", + "copy": "Хуулах", + "created_on": "Үүсгэсэн", + "delete": "Устгах", + "email": "Имэйл", + "filter": "Шүүлтүүр", + "filter_by_email": "Имэйлээр шүүнэ үү", + "id": "ID", + "load_list_error": "Хуваалцсан хүсэлтийн жагсаалтыг ачаалах боломжгүй", + "no_requests": "Хуваалцсан хүсэлт олдсонгүй", + "open_request": "Хүсэлтийг нээх", + "properties": "Үл хөдлөх хөрөнгө", + "request": "Хүсэлт", + "show_more": "Илүү ихийг харуулах", + "title": "Хуваалцсан хүсэлтүүд", + "url": "URL" + }, + "shortcut": { + "general": { + "close_current_menu": "Одоогийн цэсийг хаах", + "command_menu": "Хайлт ба тушаалын цэс", + "help_menu": "Тусламжийн цэс", + "show_all": "Гарын товчлолууд", + "title": "Генерал", + "comment_uncomment": "Сэтгэгдэл/ Тайлбарыг цуцлах", + "close_tab": "Табыг хаах", + "undo": "Буцаах", + "redo": "Дахин хий" + }, + "miscellaneous": { + "invite": "Хүмүүсийг Hoppscotch-д урь", + "title": "Төрөл бүрийн" + }, + "navigation": { + "back": "Өмнөх хуудас руу буцах", + "documentation": "Баримт бичгийн хуудас руу очно уу", + "forward": "Дараагийн хуудас руу шилжинэ үү", + "graphql": "GraphQL хуудас руу очно уу", + "profile": "Профайл хуудас руу очно уу", + "realtime": "Бодит цагийн хуудас руу очно уу", + "rest": "REST хуудас руу очно уу", + "settings": "Тохиргоо хуудас руу очно уу", + "title": "Навигац" + }, + "others": { + "prettify": "Редакторын агуулгыг сайхан болгох", + "title": "Бусад" + }, + "request": { + "delete_method": "УСТГАХ аргыг сонгоно уу", + "get_method": "GET аргыг сонгоно уу", + "head_method": "HEAD аргыг сонгоно уу", + "import_curl": "cURL импортлох", + "method": "Арга", + "next_method": "Дараагийн аргыг сонгоно уу", + "post_method": "POST аргыг сонгоно уу", + "previous_method": "Өмнөх аргыг сонгоно уу", + "put_method": "PUT аргыг сонгоно уу", + "rename": "Хүсэлтийн нэрийг өөрчлөх", + "reset_request": "Хүсэлтийг дахин тохируулах", + "save_request": "Хүсэлтийг хадгалах", + "save_to_collections": "Цуглуулгад хадгалах", + "send_request": "Хүсэлт илгээх", + "share_request": "Хүсэлт хуваалцах", + "show_code": "Кодын хэсэгчилэн үүсгэх", + "title": "Хүсэлт", + "focus_url": "URL мөрөнд анхаарлаа төвлөрүүл" + }, + "response": { + "copy": "Хариуг санах ойд хуулах", + "download": "Хариултыг файл болгон татаж авна уу", + "title": "Хариулт" + }, + "tabs": { + "title": "Таб", + "new_tab": "Шинэ таб", + "close_tab": "Табыг хаах", + "reopen_tab": "Хаалттай табыг дахин нээнэ үү", + "next_tab": "Дараагийн таб", + "previous_tab": "Өмнөх таб", + "first_tab": "Эхний таб руу шилжих", + "last_tab": "Сүүлийн таб руу шилжих", + "mru_switch": "Сүүлийн таб руу шилжих (MRU)", + "mru_switch_reverse": "Өмнөх сүүлийн таб руу шилжих (MRU)" + }, + "theme": { + "black": "Загварыг Хар горим руу шилжүүлнэ үү", + "dark": "Загварыг Харанхуй горим руу сэлгэх", + "light": "Загварыг Гэрэл горим руу сэлгэх", + "system": "Загварыг системийн горимд шилжүүлнэ үү", + "title": "Сэдэв" + } + }, + "show": { + "code": "Кодыг харуулах", + "collection": "Цуглуулгын самбарыг өргөжүүлэх", + "more": "Илүү ихийг харуулах", + "sidebar": "Хажуугийн самбарыг өргөжүүлэх", + "password": "Нууц үгээ харуулах" + }, + "socketio": { + "communication": "Харилцаа холбоо", + "connection_not_authorized": "Энэ SocketIO холболт нь ямар ч баталгаажуулалт ашигладаггүй.", + "event_name": "Үйл явдал/сэдвийн нэр", + "events": "Үйл явдал", + "log": "Бүртгэл", + "url": "URL" + }, + "spotlight": { + "change_language": "Хэлийг өөрчлөх", + "environments": { + "delete": "Одоогийн орчныг устгах", + "duplicate": "Одоогийн орчныг хуулбарлах", + "duplicate_global": "Давхардсан дэлхийн орчин", + "edit": "Одоогийн орчинг засах", + "edit_global": "Глобал орчныг засах", + "new": "Шинэ орчин бүрдүүлэх", + "new_variable": "Шинэ орчны хувьсагч үүсгэх", + "title": "Байгаль орчин" + }, + "general": { + "chat": "Дэмжлэгтэй чатлах", + "help_menu": "Тусламж, дэмжлэг", + "open_docs": "Баримт бичгийг уншина уу", + "open_github": "GitHub репозиторыг нээнэ үү", + "open_keybindings": "Гарын товчлолууд", + "social": "Нийгмийн", + "title": "Генерал" + }, + "graphql": { + "connect": "Сервертэй холбогдоно уу", + "disconnect": "Серверээс салгах" + }, + "miscellaneous": { + "invite": "Найзуудаа Hoppscotch-д урь", + "title": "Төрөл бүрийн" + }, + "request": { + "save_as_new": "Шинэ хүсэлт болгон хадгалах", + "select_method": "Арга сонгох", + "switch_to": "руу шилжих", + "tab_authorization": "Зөвшөөрлийн таб", + "tab_body": "Үндсэн таб", + "tab_headers": "Толгойн таб", + "tab_parameters": "Параметр таб", + "tab_pre_request_script": "Урьдчилан хүсэлт гаргах скрипт таб", + "tab_query": "Асуулга таб", + "tab_tests": "Туршилтын таб", + "tab_variables": "Хувьсагчийн таб" + }, + "response": { + "copy": "Хариултыг хуулах", + "download": "Хариултыг файл болгон татаж авна уу", + "title": "Хариулт" + }, + "section": { + "interceptor": "Таслагч", + "interface": "Интерфэйс", + "theme": "Сэдэв", + "user": "Хэрэглэгч" + }, + "settings": { + "change_interceptor": "Interceptor-ыг солих", + "change_language": "Хэлийг өөрчлөх", + "theme": { + "black": "Хар", + "dark": "Харанхуй", + "light": "Гэрэл", + "system": "Системийн сонголт" + } + }, + "tab": { + "close_current": "Одоогийн табыг хаах", + "close_others": "Бусад бүх табуудыг хаа", + "duplicate": "Одоогийн табыг хуулбарлах", + "new_tab": "Шинэ таб нээ", + "next": "Дараагийн таб руу шилжих", + "previous": "Өмнөх таб руу шилжих", + "switch_to_first": "Эхний таб руу шилжих", + "switch_to_last": "Сүүлийн таб руу шилжих", + "mru_switch": "Сүүлийн таб руу шилжих", + "mru_switch_reverse": "Өмнөх сүүлийн таб руу шилжих", + "title": "Таб" + }, + "workspace": { + "delete": "Одоогийн ажлын талбарыг устгах", + "edit": "Одоогийн ажлын талбарыг засах", + "invite": "Хүмүүсийг ажлын талбарт урих", + "new": "Шинэ ажлын талбар үүсгэх", + "switch_to_personal": "Хувийн ажлын талбар руугаа шилжинэ", + "title": "Ажлын талбарууд" + }, + "phrases": { + "try": "Оролдоод үзээрэй", + "import_collections": "Цуглуулга импортлох", + "create_environment": "Орчин бүрдүүлэх", + "create_workspace": "Ажлын талбар үүсгэх", + "share_request": "Хүсэлт хуваалцах" + } + }, + "sse": { + "event_type": "Үйл явдлын төрөл", + "log": "Бүртгэл", + "url": "URL" + }, + "state": { + "bulk_mode": "Бөөнөөр засварлах", + "bulk_mode_placeholder": "Бичлэгүүдийг шинэ мөрөөр тусгаарлана\nТүлхүүрүүд болон утгуудыг дараах байдлаар тусгаарлана.\nНэмэх мөрийн өмнө # гэж бичнэ үү, гэхдээ идэвхгүй байлгана уу", + "cleared": "Цэвэрлэсэн", + "connected": "Холбогдсон", + "connected_to": "{name}-д холбогдсон", + "connecting_to": "{name}-д холбогдож байна ...", + "connection_error": "Холбож чадсангүй", + "connection_failed": "Холболт амжилтгүй боллоо", + "connection_lost": "Холболт тасарсан", + "copied_interface_to_clipboard": "{language} интерфэйсийн төрлийг санах ой руу хуулсан", + "copied_to_clipboard": "Түр санах ой руу хуулсан", + "deleted": "Устгасан", + "deprecated": "ХОЦРОГДСОН", + "disabled": "Идэвхгүй", + "disconnected": "Салгасан", + "disconnected_from": "{name}-аас салсан", + "docs_generated": "Баримт бичгийг үүсгэсэн", + "download_failed": "Татаж чадсангүй", + "download_started": "Татаж эхэлсэн", + "enabled": "Идэвхжүүлсэн", + "experimental": "Туршилтын", + "file_imported": "Импортолсон файл", + "finished_in": "{duration} мс-д дууссан", + "hide": "Нуух", + "history_deleted": "Түүхийг устгасан", + "linewrap": "Мөрүүдийг боох", + "loading": "Ачааж байна...", + "message_received": "Зурвас: {message} сэдэв дээр ирсэн: {topic}", + "mqtt_subscription_failed": "Сэдвийг захиалах явцад алдаа гарлаа: {topic}", + "no_content_found": "Агуулга олдсонгүй", + "none": "Байхгүй", + "nothing_found": "Юу ч олдсонгүй", + "published_error": "{topic} сэдэв рүү мессеж нийтлэх явцад алдаа гарлаа: {message}", + "published_message": "Нийтэлсэн зурвас: {message} сэдэв: {topic}", + "reconnection_error": "Дахин холбогдож чадсангүй", + "saved": "Хадгалсан", + "show": "Үзүүлэх", + "subscribed_failed": "Сэдвийг захиалж чадсангүй: {topic}", + "subscribed_success": "Сэдвийг амжилттай бүртгүүлсэн: {topic}", + "unsubscribed_failed": "Сэдвээс бүртгэлээ хасаж чадсангүй: {topic}", + "unsubscribed_success": "Сэдвээс бүртгэлээ амжилттай цуцаллаа: {topic}", + "waiting_send_request": "Хүсэлт илгээхийг хүлээж байна", + "user_deactivated": "Таны бүртгэл идэвхгүй болсон байна. Бүртгэлээ дахин идэвхжүүлэхийн тулд админтай холбогдоно уу.", + "add_user_failure": "Хэрэглэгчийг ажлын талбарт нэмж чадсангүй!!", + "add_user_success": "Хэрэглэгч одоо ажлын талбарын гишүүн боллоо!!", + "admin_failure": "Хэрэглэгчийг админ болгож чадсангүй!!", + "admin_success": "Хэрэглэгч одоо админ боллоо!!", + "and": "болон", + "clear_selection": "Сонголтыг арилгах", + "configure_auth": "Баталгаажуулах үйлчилгээ үзүүлэгчийг админ тохиргооноос тохируулна уу эсвэл баримт бичгийг шалгана уу.", + "confirm_admin_to_user": "Та энэ хэрэглэгчээс админ статусыг устгахыг хүсэж байна уу?", + "confirm_admins_to_users": "Та сонгосон хэрэглэгчдээс админ статусыг устгахыг хүсэж байна уу?", + "confirm_delete_infra_token": "Та {tokenLabel} infra жетоныг устгахдаа итгэлтэй байна уу?", + "confirm_delete_invite": "Та сонгосон урилгыг хүчингүй болгохыг хүсэж байна уу?", + "confirm_delete_invites": "Та сонгосон урилгыг цуцлахыг хүсэж байна уу?", + "confirm_user_deletion": "Хэрэглэгчийн устгалыг баталгаажуулах уу?", + "confirm_users_deletion": "Та сонгосон хэрэглэгчдийг устгахыг хүсэж байна уу?", + "confirm_user_to_admin": "Та энэ хэрэглэгчийг админ болгохыг хүсэж байна уу?", + "confirm_users_to_admin": "Та сонгосон хэрэглэгчдийг админ болгохыг хүсэж байна уу?", + "confirm_user_deactivation": "Хэрэглэгчийг идэвхгүй болгохыг баталгаажуулах уу?", + "confirm_logout": "Гарахыг баталгаажуулна уу", + "created_on": "Үүсгэсэн", + "continue_email": "Цахим шуудангаар үргэлжлүүлнэ үү", + "continue_github": "Github-ийг үргэлжлүүлнэ үү", + "continue_google": "Google-тэй үргэлжлүүлнэ үү", + "continue_microsoft": "Майкрософттой үргэлжлүүлнэ үү", + "create_team_failure": "Ажлын талбар үүсгэж чадсангүй!!", + "create_team_success": "Ажлын талбарыг амжилттай үүсгэсэн!!", + "data_sharing_failure": "Өгөгдөл хуваалцах тохиргоог шинэчилж чадсангүй", + "delete_infra_token_failure": "Инфра токеныг устгах явцад алдаа гарлаа", + "delete_invite_failure": "Урилгыг устгаж чадсангүй!!", + "delete_invites_failure": "Сонгосон урилгыг устгаж чадсангүй!!", + "delete_invite_success": "Урилгыг амжилттай устгалаа!!", + "delete_invites_success": "Сонгосон урилгыг амжилттай устгалаа!!", + "delete_request_failure": "Хуваалцсан хүсэлтийг устгаж чадсангүй!!", + "delete_request_success": "Хуваалцсан хүсэлтийг амжилттай устгалаа!!", + "delete_team_failure": "Ажлын талбарыг устгаж чадсангүй!!", + "delete_team_success": "Ажлын талбарыг амжилттай устгалаа!!", + "delete_some_users_failure": "Устаагүй хэрэглэгчдийн тоо: {count}", + "delete_some_users_success": "Устгасан хэрэглэгчдийн тоо: {count}", + "delete_user_failed_only_one_admin": "Хэрэглэгчийг устгаж чадсангүй. Дор хаяж нэг админ байх ёстой!!", + "delete_user_failure": "Хэрэглэгчийг устгаж чадсангүй!!", + "delete_users_failure": "Сонгосон хэрэглэгчдийг устгаж чадсангүй!!", + "delete_user_success": "Хэрэглэгчийг амжилттай устгалаа!!", + "delete_users_success": "Сонгосон хэрэглэгчдийг амжилттай устгалаа!!", + "email": "Имэйл", + "email_failure": "Урилга илгээж чадсангүй", + "email_signin_failure": "Имэйлээр нэвтэрч чадсангүй", + "email_success": "Имэйл урилгыг амжилттай илгээв", + "emails_cannot_be_same": "Та өөрийгөө урих боломжгүй, өөр имэйл хаяг сонгоно уу!!", + "enter_team_email": "Ажлын талбар эзэмшигчийн имэйл хаягийг оруулна уу!!", + "error": "Ямар нэг алдаа гарлаа", + "error_auth_providers": "Баталгаажуулах үйлчилгээ үзүүлэгчдийг ачаалах боломжгүй", + "generate_infra_token_failure": "Инфра жетон үүсгэх явцад алдаа гарлаа", + "github_signin_failure": "Github-ээр нэвтэрч чадсангүй", + "google_signin_failure": "Google-ээр нэвтэрч чадсангүй", + "infra_token_label_short": "Infra Token Label тэмдэгтийн урт хэт богино байна!!", + "invalid_email": "Хүчинтэй имэйл хаяг оруулна уу", + "link_copied_to_clipboard": "Холбоосыг санах ой руу хуулсан", + "logged_out": "Гарсан", + "login_as_admin": "болон админ бүртгэлээр нэвтэрнэ үү.", + "login_using_email": "Хэрэглэгчээс имэйлээ шалгах эсвэл доорх холбоосыг хуваалцахыг хүснэ үү", + "login_using_link": "Доорх линкээр нэвтэрч орохыг хэрэглэгчээс хүснэ үү", + "logout": "Гарах", + "magic_link_sign_in": "Холбоос дээр дарж нэвтэрнэ үү.", + "magic_link_success": "Бид шидэт холбоос илгээсэн", + "microsoft_signin_failure": "Microsoft-оор нэвтэрч чадсангүй", + "newsletter_failure": "Мэдээллийн товхимолын тохиргоог шинэчлэх боломжгүй байна", + "non_admin_logged_in": "Админ бус хэрэглэгчээр нэвтэрсэн.", + "non_admin_login": "Та нэвтэрсэн байна. Гэхдээ та админ биш", + "owner_not_present": "Ядаж нэг эзэн багт байх ёстой!!", + "privacy_policy": "Нууцлалын бодлого", + "reenter_email": "Имэйлээ дахин оруулна уу", + "remove_admin_failure": "Админ статусыг устгаж чадсангүй!!", + "remove_admin_failure_only_one_admin": "Админ статусыг устгаж чадсангүй. Дор хаяж нэг админ байх ёстой!!", + "remove_admin_success": "Админ статус хасагдсан!!", + "remove_admin_from_users_failure": "Сонгогдсон хэрэглэгчдээс админ статусыг устгаж чадсангүй!!", + "remove_admin_from_users_success": "Сонгогдсон хэрэглэгчдээс админ статусыг хассан!!", + "remove_admin_to_delete_user": "Хэрэглэгчийг устгахын тулд админы эрхийг хасна уу!!", + "remove_owner_to_delete_user": "Хэрэглэгчийг устгахын тулд багийн эзэмшлийн статусыг устгана уу!!", + "remove_owner_failure_only_one_owner": "Гишүүнийг хасаж чадсангүй. Нэг багт дор хаяж нэг эзэн байх ёстой!!", + "remove_admin_for_deletion": "Устгахыг оролдохоос өмнө админ статусыг устгана уу!!", + "remove_owner_for_deletion": "Нэг буюу хэд хэдэн хэрэглэгчид багийн эзэд байна. Устгахаас өмнө өмчлөлийг шинэчилнэ үү!!", + "remove_invitee_failure": "Урьсан хүнийг хасаж чадсангүй!!", + "remove_invitee_success": "Урьсан хүнийг устгалаа.", + "remove_member_failure": "Гишүүнийг хасаж чадсангүй!!", + "remove_member_success": "Гишүүнийг амжилттай хаслаа!!", + "rename_team_failure": "Ажлын талбарын нэрийг өөрчилж чадсангүй!!", + "rename_team_success": "Ажлын талбайн нэрийг амжилттай өөрчилсөн!", + "rename_user_failure": "Хэрэглэгчийн нэрийг өөрчилж чадсангүй!!", + "rename_user_success": "Хэрэглэгчийн нэрийг амжилттай өөрчилсөн!!", + "require_auth_provider": "Нэвтрэхийн тулд та дор хаяж нэг баталгаажуулалтын үйлчилгээ үзүүлэгч тохируулах шаардлагатай.", + "role_update_failed": "Дүрүүдийн шинэчлэл амжилтгүй боллоо!!", + "role_update_success": "Дүрүүдийг амжилттай шинэчилсэн!!", + "selected": "{count} сонгосон", + "self_host_docs": "Өөрөө хостын баримт бичиг", + "send_magic_link": "Шидэт холбоос илгээх", + "setup_failure": "Тохиргоо амжилтгүй боллоо!!", + "setup_success": "Тохиргоо амжилттай дууссан!!", + "sign_in_agreement": "Нэвтрэн орсноор та манайхыг зөвшөөрч байна", + "sign_in_options": "Бүгд нэвтрэх сонголт", + "sign_out": "Гарах", + "something_went_wrong": "Ямар нэг алдаа гарлаа", + "team_name_too_short": "Ажлын талбарын нэр дор хаяж 6 тэмдэгтээс бүрдэх ёстой!!", + "user_already_invited": "Урилгыг илгээж чадсангүй. Хэрэглэгчийг аль хэдийн урьсан байна!!", + "user_not_found": "Хэрэглэгчийг инфра-д олоогүй байна!!", + "users_to_admin_success": "Сонгогдсон хэрэглэгчдийг админ статустай болгож байна!!", + "users_to_admin_failure": "Сонгогдсон хэрэглэгчдийг админы статустай болгож чадсангүй!!", + "loading_workspaces": "Ажлын талбаруудыг ачаалж байна", + "loading_collections_in_workspace": "Ажлын талбарт цуглуулгуудыг ачаалж байна" + }, + "support": { + "changelog": "Хамгийн сүүлийн үеийн хувилбаруудын талаар дэлгэрэнгүй уншина уу", + "chat": "Асуулт? Бидэнтэй чатлаарай!", + "community": "Асуулт асууж, бусдад туслаарай", + "documentation": "Hoppscotch-ийн талаар дэлгэрэнгүй уншина уу", + "forum": "Асуулт асууж, хариулт аваарай", + "github": "Github дээр биднийг дагаарай", + "shortcuts": "Аппликешныг илүү хурдан үзэх", + "title": "Дэмжлэг", + "twitter": "Twitter дээр биднийг дагаарай" + }, + "tab": { + "authorization": "Зөвшөөрөл", + "body": "Бие", + "close": "Табыг хаах", + "close_others": "Бусад цонхыг хаах", + "collections": "Цуглуулга", + "documentation": "Баримт бичиг", + "duplicate": "Давхардсан таб", + "environments": "Байгаль орчин", + "headers": "Гарчиг", + "history": "Түүх", + "mqtt": "MQTT", + "parameters": "Параметрүүд", + "post_request_script": "Хүсэлтийн дараах скрипт", + "pre_request_script": "Скриптийг урьдчилан хүсэх", + "scripts": "Скриптүүд", + "queries": "Асуултууд", + "query": "Асуулга", + "schema": "Схем", + "shared_requests": "Хуваалцсан хүсэлтүүд", + "codegen": "Код үүсгэх", + "code_snippet": "Кодын хэсэг", + "mock_servers": "Хуурамч серверүүд", + "share_tab_request": "Табын хүсэлтийг хуваалцах", + "socketio": "Socket.IO", + "sse": "SSE", + "types": "Төрөл", + "variables": "Хувьсагч", + "websocket": "WebSocket", + "all_tests": "Бүх тестүүд", + "passed": "Давсан", + "failed": "Амжилтгүй" + }, + "team": { + "activity_logs": "Үйл ажиллагааны бүртгэл", + "already_member": "Энэ имэйл одоо байгаа хэрэглэгчтэй холбоотой байна.", + "create_new": "Шинэ ажлын талбар үүсгэх", + "deleted": "Ажлын талбарыг устгасан", + "delete_all_activity_logs": "Бүх үйл ажиллагааны бүртгэлийг устгана уу", + "successfully_deleted_all_activity_logs": "Бүх үйл ажиллагааны бүртгэлийг амжилттай устгасан", + "delete_activity_log": "Үйл ажиллагааны бүртгэлийг устгах", + "deleted_activity_log": "Сонгосон үйл ажиллагааны бүртгэлийг устгасан", + "deleted_all_activity_logs": "Бүх үйл ажиллагааны бүртгэлийг устгасан", + "edit": "Ажлын талбарыг засах", + "email": "И-мэйл", + "email_do_not_match": "Имэйл таны бүртгэлийн мэдээлэлтэй таарахгүй байна. Ажлын талбайн эзэнтэй холбогдоно уу.", + "exit": "Ажлын талбараас гарах", + "exit_disabled": "Зөвхөн эзэмшигч нь ажлын талбараас гарах боломжгүй", + "failed_invites": "Амжилтгүй урилга", + "invalid_coll_id": "Цуглуулгын ID буруу", + "invalid_email_format": "Имэйлийн формат буруу байна", + "invalid_id": "Ажлын талбарын ID буруу. Ажлын талбайн эзэнтэй холбогдоно уу.", + "invalid_invite_link": "Урилгын холбоос буруу байна", + "invalid_invite_link_description": "Таны дагасан холбоос буруу байна. Ажлын талбайн эзэнтэй холбогдоно уу.", + "invalid_member_permission": "Ажлын талбарын гишүүнд хүчинтэй зөвшөөрөл өгнө үү", + "invite": "Урих", + "invite_more": "Илүү ихийг урих", + "invite_tooltip": "Хүмүүсийг энэ ажлын талбарт урь", + "invited_to_team": "{owner} таныг {workspace}-д нэгдэхийг урьсан.", + "join": "Урилгыг хүлээн авлаа", + "join_team": "{workspace}-д нэгдэх", + "joined_team": "Та {workspace}-д нэгдсэн", + "joined_team_description": "Та одоо энэ ажлын талбарын гишүүн боллоо", + "left": "Та ажлын талбараас гарлаа", + "login_to_continue": "Үргэлжлүүлэхийн тулд нэвтэрнэ үү", + "login_to_continue_description": "Та ажлын талбарт нэгдэхийн тулд нэвтэрсэн байх шаардлагатай.", + "logout_and_try_again": "Гараад өөр бүртгэлээр нэвтэрнэ үү", + "member_has_invite": "Хэрэглэгч аль хэдийн урилгатай байна. Тэднээс ирсэн имэйлээ шалгах эсвэл урилгыг хүчингүй болгож дахин илгээхийг хүснэ үү.", + "member_not_found": "Гишүүн олдсонгүй. Ажлын талбайн эзэнтэй холбогдоно уу.", + "member_removed": "Хэрэглэгчийг устгасан", + "member_role_updated": "Хэрэглэгчийн үүрэг шинэчлэгдсэн", + "members": "Гишүүд", + "more_members": "+{count} өөр", + "name_length_insufficient": "Ажлын талбарын нэр хоосон байж болохгүй", + "name_updated": "Ажлын талбарын нэрийг шинэчилсэн", + "new": "Шинэ ажлын талбар", + "new_created": "Шинэ ажлын талбар бий болсон", + "new_name": "Миний шинэ ажлын талбар", + "no_access": "Танд энэ ажлын талбарт засварлах эрх байхгүй", + "no_invite_found": "Урилга олдсонгүй. Ажлын талбайн эзэнтэй холбогдоно уу.", + "no_request_found": "Хүсэлт олдсонгүй.", + "not_found": "Ажлын талбар олдсонгүй. Ажлын талбайн эзэнтэй холбогдоно уу.", + "not_valid_viewer": "Та хүчинтэй үзэгч биш байна. Ажлын талбайн эзэнтэй холбогдоно уу.", + "parent_coll_move": "Цуглуулгыг хүүхдийн цуглуулга руу зөөх боломжгүй", + "pending_invites": "Хүлээгдэж буй урилгууд", + "permissions": "Зөвшөөрөл", + "same_target_destination": "Нэг зорилго, хүрэх газар", + "saved": "Ажлын талбарыг хадгалсан", + "select_a_team": "Ажлын талбарыг сонгоно уу", + "success_invites": "Амжилтыг урьж байна", + "title": "Ажлын талбарууд", + "we_sent_invite_link": "Урилгууд замдаа байна", + "invite_sent_smtp_disabled": "Урилгын холбоос үүсгэсэн", + "we_sent_invite_link_description": "Шинэ уригдсан хүмүүс ажлын талбарт нэгдэх холбоосыг хүлээн авах бөгөөд одоо байгаа гишүүд болон хүлээгдэж буй уригчид шинэ холбоос хүлээн авахгүй.", + "invite_sent_smtp_disabled_description": "Энэ Hoppscotch жишээний хувьд урилга имэйл илгээхийг идэвхгүй болгосон. Урилгын холбоосыг гараар хуулж, хуваалцахын тулд \"Холбоос хуулах\" товчийг ашиглана уу.", + "copy_invite_link": "Урилгын холбоосыг хуулах", + "search_title": "Багийн хүсэлт", + "user_not_found": "Энэ тохиолдолд хэрэглэгч олдсонгүй.", + "invite_members": "Гишүүдийг урих" + }, + "team_environment": { + "deleted": "Хүрээлэн буй орчныг устгасан", + "duplicate": "Хүрээлэн буй орчин давхардсан", + "not_found": "Байгаль орчин олдсонгүй." + }, + "test": { + "requests": "Хүсэлтүүд", + "selection": "Сонголт", + "failed": "туршилт амжилтгүй болсон", + "javascript_code": "JavaScript код", + "learn": "Баримт бичгийг уншина уу", + "passed": "шалгалтад тэнцсэн", + "report": "Туршилтын тайлан", + "results": "Туршилтын үр дүн", + "script": "Скрипт", + "snippets": "Хэсэг хэсгүүд", + "run": "Гүй", + "run_again": "Дахин гүй", + "stop": "Зогс", + "new_run": "Шинэ гүйлт", + "iterations": "Давталт", + "duration": "Үргэлжлэх хугацаа", + "avg_resp": "Дундаж. Хариу өгөх хугацаа" + }, + "websocket": { + "communication": "Харилцаа холбоо", + "log": "Бүртгэл", + "message": "Мессеж", + "protocols": "Протоколууд", + "url": "URL" + }, + "workspace": { + "change": "Ажлын талбарыг өөрчлөх", + "personal": "Хувийн ажлын талбар", + "other_workspaces": "Миний ажлын талбарууд", + "team": "Ажлын талбар", + "title": "Ажлын талбарууд" + }, + "site_protection": { + "login_to_continue": "Үргэлжлүүлэхийн тулд нэвтэрнэ үү", + "login_to_continue_description": "Энэ Hoppscotch Enterprise Instance-д хандахын тулд та нэвтэрсэн байх шаардлагатай.", + "error_fetching_site_protection_status": "Сайтын хамгаалалтын статусыг дуудаж байхад ямар нэг алдаа гарлаа" + }, + "access_tokens": { + "tab_title": "Токенууд", + "section_title": "Хувийн хандалтын токенууд", + "section_description": "Хувийн хандалтын токенууд нь танд CLI-г Hoppscotch дансандаа холбоход тусална", + "last_used_on": "Хамгийн сүүлд ашигласан", + "expires_on": "Дуусах өдөр", + "no_expiration": "Хугацаа дуусахгүй", + "expired": "Хугацаа дууссан", + "copy_token_warning": "Хувийн хандалтын токеноо яг одоо хуулахаа мартуузай. Та үүнийг дахиж харах боломжгүй болно!", + "token_purpose": "Энэ токен юунд зориулагдсан бэ?", + "expiration_label": "Хугацаа дуусах", + "scope_label": "Хамрах хүрээ", + "workspace_read_only_access": "Ажлын талбарын өгөгдөлд зөвхөн унших боломжтой.", + "personal_workspace_access_limitation": "Хувийн хандалтын токенууд таны хувийн ажлын талбарт хандах боломжгүй.", + "generate_token": "Токен үүсгэх", + "invalid_label": "Токены шошгыг өгнө үү", + "no_expiration_verbose": "Энэ токен хэзээ ч дуусахгүй!", + "token_expires_on": "Энэ токены хугацаа дуусах болно", + "generate_new_token": "Шинэ токен үүсгэх", + "generate_modal_title": "Шинэ хувийн хандалтын токен", + "deletion_success": "Хандалтын токен {label} устгагдсан" + }, + "collection_runner": { + "collection_id": "Цуглуулгын ID", + "environment_id": "Байгаль орчны ID", + "cli_collection_id_description": "Энэ цуглуулгын ID-г CLI цуглуулагч Hoppscotch-д ашиглах болно.", + "cli_environment_id_description": "Энэ орчны ID-г Hoppscotch-д зориулсан CLI цуглуулга гүйгч ашиглах болно.", + "include_active_environment": "Идэвхтэй орчинг оруулах:", + "cli": "CLI", + "cli_comming_soon_for_personal_collection": "CLI дахь хувийн цуглуулгуудад зориулсан Collection Runner удахгүй гарах болно.", + "delay": "Саатал", + "negative_delay": "Саатал сөрөг байж болохгүй", + "ui": "Гүйгч", + "running_collection": "Ажиллаж байгаа цуглуулга", + "run_config": "Тохиргоог ажиллуул", + "advanced_settings": "Нарийвчилсан тохиргоо", + "stop_on_error": "Алдаа гарвал ажиллуулахаа зогсоо", + "persist_responses": "Тогтвортой хариултууд", + "keep_variable_values": "Хувьсах утгуудыг хадгалах", + "collection_not_found": "Цуглуулга олдсонгүй. Устгаж эсвэл зөөж болно.", + "empty_collection": "Цуглуулга хоосон байна. Ажиллуулах хүсэлтийг нэмнэ үү.", + "no_response_persist": "Цуглуулгын гүйгчийг одоогоор хариу өгөхгүй байхаар тохируулсан байна. Энэ тохиргоо нь хариултын өгөгдлийг харуулахаас сэргийлдэг. Энэ зан үйлийг өөрчлөхийн тулд шинэ ажиллуулах тохиргоог эхлүүлнэ үү.", + "select_request": "Хариулт болон туршилтын үр дүнг харахын тулд хүсэлтийг сонгоно уу", + "response_body_lost_rerun": "Хариуцлагын бие алга болсон. Хариултыг авахын тулд цуглуулгыг дахин ажиллуулна уу.", + "cli_command_generation_description_cloud": "Доорх командыг хуулж CLI-аас ажиллуулна уу. Хувийн хандалтын токеныг зааж өгнө үү.", + "cli_command_generation_description_sh": "Доорх командыг хуулж CLI-аас ажиллуулна уу. Хувийн хандалтын токеныг зааж, үүсгэсэн SH жишээ серверийн URL-г баталгаажуулна уу.", + "cli_command_generation_description_sh_with_server_url_placeholder": "Доорх командыг хуулж CLI-аас ажиллуулна уу. Хувийн хандалтын токен болон SH жишээ серверийн URL-г зааж өгнө үү.", + "run_collection": "Цуглуулга ажиллуулах", + "no_passed_tests": "Ямар ч шалгалт өгөөгүй", + "no_failed_tests": "Ямар ч туршилт амжилтгүй болсон" + }, + "ai_experiments": { + "generate_request_name": "AI ашиглан хүсэлтийн нэр үүсгэх", + "generate_or_modify_request_body": "Хүсэлтийн хэсгийг үүсгэх эсвэл өөрчлөх", + "modify_with_ai": "AI ашиглан өөрчлөх", + "generate": "Үүсгэх", + "generate_or_modify_request_body_input_placeholder": "Хүсэлтийн үндсэн хэсгийг өөрчлөх хүсэлтээ оруулна уу", + "accept_change": "Өөрчлөлтийг хүлээн зөвшөөрөх", + "feedback_success": "Санал хүсэлтийг амжилттай илгээсэн", + "feedback_failure": "Санал хүсэлт илгээж чадсангүй", + "feedback_thank_you": "Санал хүсэлтээ өгсөнд баярлалаа!", + "feedback_cta_text_long": "Үе үеийг үнэл, биднийг сайжруулахад тусалдаг", + "feedback_cta_request_name": "Үүсгэсэн нэр танд таалагдсан уу?", + "modify_request_body_error": "Хүсэлтийн үндсэн хэсгийг өөрчилж чадсангүй", + "generate_or_modify_prerequest_input_placeholder": "Урьдчилсан хүсэлтийн скриптийг үүсгэх эсвэл өөрчлөх хүсэлтийг оруулна уу", + "generate_or_modify_post_request_script_input_placeholder": "Хүсэлтийн дараах скриптийг үүсгэх эсвэл өөрчлөх хүсэлтийг оруулна уу", + "modify_post_request_script_error": "Хүсэлтийн дараах скриптийг өөрчилж чадсангүй", + "modify_prerequest_error": "Урьдчилсан хүсэлтийн скриптийг өөрчилж чадсангүй" + }, + "configs": { + "auth_providers": { + "callback_url": "БУЦАХ URL", + "client_id": "CLIENT ID", + "client_secret": "ҮЙЛЧЛҮҮЛЭГЧИЙН НУУЦ", + "description": "Сервертээ баталгаажуулах үйлчилгээ үзүүлэгчдийг тохируулна уу", + "provider_not_specified": "Дор хаяж нэг баталгаажуулалтын үйлчилгээ үзүүлэгчийг идэвхжүүлнэ үү", + "scope": "ХАМРАХ ХҮРЭЭ", + "tenant": "ТҮРЭЭЛЭГЧ", + "title": "Баталгаажуулах үйлчилгээ үзүүлэгчид", + "update_failure": "Баталгаажуулалтын үйлчилгээ үзүүлэгчийн тохиргоог шинэчилж чадсангүй!!" + }, + "confirm_changes": "Шинэ өөрчлөлтүүдийг тусгахын тулд Hoppscotch серверийг дахин эхлүүлэх шаардлагатай. Серверийн тохиргоонд хийсэн өөрчлөлтийг баталгаажуулах уу?", + "input_empty": "Тохиргоог шинэчлэхийн өмнө бүх талбарыг бөглөнө үү", + "data_sharing": { + "title": "Өгөгдөл хуваалцах", + "description": "Нэргүй өгөгдөл хуваалцаж Hoppscotch-г сайжруулахад тусална уу", + "enable": "Мэдээлэл хуваалцахыг идэвхжүүлнэ үү", + "secondary_title": "Өгөгдөл хуваалцах тохиргоо", + "see_shared": "Юу хуваалцаж байгааг хараарай", + "toggle_description": "Нэргүй өгөгдлийг хуваалцах", + "update_failure": "Өгөгдөл хуваалцах тохиргоог шинэчилж чадсангүй!!" + }, + "load_error": "Серверийн тохиргоог ачаалах боломжгүй байна", + "mail_configs": { + "address_from": "ХАЯГСАС ШУУД", + "custom_smtp_configs": "Тусгай SMTP тохиргоог ашиглах", + "description": "smtp тохиргоог тохируулна уу", + "enable_email_auth": "Имэйлд суурилсан баталгаажуулалтыг идэвхжүүлнэ үү", + "enable_smtp": "SMTP-г идэвхжүүлнэ үү", + "host": "Шуудангийн эзэн", + "password": "Шуудангийн нууц үг", + "port": "Шуудангийн порт", + "secure": "МАЙЛЕРИЙН АЮУЛГҮЙ", + "smtp_url": "MAILER SMTP URL", + "tls_reject_unauthorized": "TLS ЗӨВШӨӨРӨГӨӨГҮЙ ТАТГАЛЖ БАЙНА", + "title": "SMTP тохиргоо", + "toggle_failure": "smtp-г асааж чадсангүй!!", + "update_failure": "smtp тохиргоог шинэчилж чадсангүй!!", + "user": "МАЙЛАН ХЭРЭГЛЭГЧ" + }, + "reset": { + "confirm_reset": "Шинэ өөрчлөлтүүдийг тусгахын тулд Hoppscotch серверийг дахин эхлүүлэх шаардлагатай. Серверийн тохиргоог дахин тохируулахыг баталгаажуулах уу?", + "description": "Өгөгдмөл тохиргоонууд нь орчны файлд заасны дагуу ачаалагдах болно", + "failure": "Тохиргоог дахин тохируулж чадсангүй!!", + "title": "Тохиргоог дахин тохируулах", + "info": "Серверийн тохиргоог дахин тохируулах" + }, + "restart": { + "description": "Энэ хуудсыг автоматаар дахин ачаалахад {duration} секунд үлдлээ", + "initiate": "Серверийг дахин эхлүүлэхийг эхлүүлж байна...", + "title": "Сервер дахин эхэлж байна" + }, + "save_changes": "Өөрчлөлтүүдийг хадгалах", + "title": "Тохиргоо", + "update_failure": "Серверийн тохиргоог шинэчилж чадсангүй", + "restrict_access": "Хандалтыг хязгаарлах", + "site_protection": { + "control_access": "Hoppscotch програмд хэн хандах боломжтойг хянах", + "description": "Сайтын хамгаалалтын тохиргоог ашиглан зочдод таны Hoppscotch програмд хэрхэн хандахыг тохируулаарай.", + "enable": "Сайтын хамгаалалтыг идэвхжүүлэх", + "note": "Hoppscotch аппликейшнд зочилсон хэрэглэгчид нэвтрэх хуудсыг харах бөгөөд програмд нэвтрэхийн тулд заавал нэвтрэх шаардлагатай. Зөвшөөрөл авахын тулд админы зөвшөөрөл шаардлагатай хэвээр байна", + "update_failure": "Сайтын хамгаалалтын тохиргоог шинэчилж чадсангүй!!" + }, + "domain_whitelisting": { + "add_domain": "Шинэ домэйн нэмэх", + "description": "Зөвшөөрөгдсөн домэйнд бүртгэлтэй цахим шуудангийн ID-тай хэрэглэгчид Hoppscotch програм руу нэвтрэхийн тулд админаас тодорхой зөвшөөрөл шаарддаггүй.", + "enable": "Домэйн цагаан жагсаалтыг идэвхжүүлэх", + "enter_domain": "Домэйн оруулна уу", + "title": "Зөвшөөрөгдсөн жагсаалтад орсон домэйнууд", + "toggle_failure": "Домэйн зөвшөөрөгдсөн жагсаалтыг сэлгэж чадсангүй", + "update_failure": "Домэйн зөвшөөрөгдсөн жагсаалтын тохиргоог шинэчилж чадсангүй!!" + }, + "oidc_configs": { + "auth_url": "Auth URL", + "callback_url": "Буцах URL", + "client_id": "Үйлчлүүлэгчийн ID", + "client_secret": "Үйлчлүүлэгчийн нууц", + "description": "OIDC тохиргоог тохируулах", + "enable": "OIDC дээр суурилсан баталгаажуулалтыг идэвхжүүлнэ", + "issuer": "Үнэт цаас гаргагч", + "provider_name": "Үйлчилгээ үзүүлэгчийн нэр", + "scope": "Хамрах хүрээ", + "title": "OIDC тохиргоо", + "token_url": "Токен URL", + "update_failure": "OIDC тохиргоог шинэчилж чадсангүй!!", + "user_info_url": "Хэрэглэгчийн мэдээллийн URL" + }, + "saml": { + "audience": "Үзэгчид", + "callback_url": "Буцах URL", + "certificate": "Сертификат", + "description": "SAML тохиргоог тохируулах", + "enable": "SAML-д суурилсан баталгаажуулалтыг идэвхжүүлнэ үү", + "entry_point": "Нэвтрэх цэг", + "issuer": "Үнэт цаас гаргагч", + "title": "SAML тохиргоо", + "update_failure": "SAML SSO тохиргоог шинэчилж чадсангүй!!", + "want_assertions_signed": "Баталгаажуулалтад гарын үсэг зурах", + "want_response_signed": "Гарын үсэг зурах хариу" + } + }, + "data_sharing": { + "description": "Hoppscotch-ийг сайжруулахын тулд нэргүй дата ашиглалтыг хуваалцаарай", + "enable": "Мэдээлэл хуваалцахыг идэвхжүүлнэ үү", + "see_shared": "Юу хуваалцаж байгааг хараарай", + "toggle_description": "Өгөгдлийг хуваалцаж, Hoppscotch-ийг илүү сайн болго", + "title": "Хоппскочийг илүү сайн болго", + "welcome": "тавтай морил" + }, + "infra_tokens": { + "copy_token_warning": "Инфра токеноо яг одоо хуулж байгаарай. Та үүнийг дахиж харах боломжгүй болно!", + "deletion_success": "Инфра токен {label} устгагдсан", + "empty": "Инфра жетон хоосон байна", + "expired": "Хугацаа дууссан", + "expiration_label": "Хугацаа дуусах", + "expires_on": "Дуусах өдөр", + "generate_modal_title": "Шинэ инфра токен", + "generate_new_token": "Шинэ токен үүсгэх", + "generate_token": "Токен үүсгэх", + "invalid_label": "Токены шошгыг өгнө үү", + "last_used_on": "Хамгийн сүүлд ашигласан", + "no_expiration": "Хугацаа дуусахгүй", + "no_expiration_verbose": "Энэ токен хэзээ ч дуусахгүй!", + "section_description": "Infra жетон бүхий API-уудаар дамжуулан Hoppscotch хэрэглэгчидээ удирдаарай", + "section_title": "Инфра жетон", + "tab_title": "Инфра жетон", + "token_expires_on": "Энэ токены хугацаа дуусах болно", + "token_purpose": "Энэ тэмдгийг тодорхойлохын тулд шошго оруулна уу" + }, + "metrics": { + "dashboard": "Хяналтын самбар", + "no_metrics": "Ямар ч үзүүлэлт олдсонгүй", + "total_collections": "Нийт цуглуулга", + "total_requests": "Нийт хүсэлт", + "total_teams": "Нийт ажлын талбай", + "total_users": "Нийт хэрэглэгчид" + }, + "newsletter": { + "description": "Манай хамгийн сүүлийн үеийн мэдээний талаар мэдээлэл аваарай", + "subscribe": "Бүртгүүлэх", + "title": "Холбоотой байгаарай", + "toggle_description": "Hoppscotch-ээс хамгийн сүүлийн үеийн мэдээг аваарай", + "unsubscribe": "Бүртгэлээ цуцлах" + }, + "teams": { + "add_member": "Гишүүн нэмэх", + "add_members": "Гишүүн нэмэх", + "add_new": "Шинэ нэмэх", + "admin": "Админ", + "admin_Email": "Админ Имэйл", + "admin_id": "Админ ID", + "cancel": "Цуцлах", + "confirm_team_deletion": "Ажлын талбарыг устгахыг баталгаажуулах уу?", + "copy": "Хуулах", + "create_team": "Ажлын талбар үүсгэх", + "date": "Огноо", + "delete_team": "Ажлын талбарыг устгах", + "details": "Дэлгэрэнгүй мэдээлэл", + "edit": "Засварлах", + "editor": "РЕДАКТОР", + "editor_description": "Редакторууд хүсэлт, цуглуулга нэмэх, засах, устгах боломжтой.", + "email": "Ажлын талбар эзэмшигчийн имэйл", + "email_address": "Имэйл хаяг", + "email_title": "Имэйл", + "empty_name": "Багийн нэр хоосон байж болохгүй!!", + "error": "Ямар нэг алдаа гарлаа. Дараа дахин оролдоно уу.", + "id": "Ажлын талбарын ID", + "invited_email": "Урьсан имэйл", + "invited_on": "Уригдсан", + "invites": "урьж байна", + "load_info_error": "Workspace-н мэдээллийг ачаалах боломжгүй", + "load_list_error": "Ажлын талбарын жагсаалтыг ачаалах боломжгүй", + "members": "Гишүүдийн тоо", + "no_invite": "Урилга байхгүй", + "no_invite_description": "Хамтран ажиллаж эхлэхэд багаа урь", + "owner": "ЭЗЭН", + "owner_description": "Эзэмшигчид хүсэлт, цуглуулга, ажлын талбарын гишүүдийг нэмэх, засах, устгах боломжтой.", + "permissions": "Зөвшөөрөл", + "name": "Ажлын талбарын нэр", + "no_members": "Энэ ажлын талбарт гишүүн алга. Хамтран ажиллахын тулд энэ ажлын талбарт гишүүд нэмнэ үү", + "no_pending_invites": "Хүлээгдэж буй урилга байхгүй", + "no_teams": "Ажлын талбар олдсонгүй.", + "no_teams_description": "Багтайгаа хамтран ажиллах ажлын талбарыг бий болго", + "pending_invites": "Хүлээгдэж буй урилгууд", + "roles": "Дүрүүд", + "roles_description": "Хуваалцсан цуглуулгад хандах хандалтыг хянахын тулд дүрүүдийг ашигладаг.", + "remove": "Устгах", + "rename": "Нэрээ өөрчлөх", + "save": "Хадгалах", + "save_changes": "Өөрчлөлтүүдийг хадгалах", + "send_invite": "Урилга илгээх", + "show_more": "Илүү ихийг харуулах", + "team_details": "Ажлын байрны дэлгэрэнгүй мэдээлэл", + "team_members": "Гишүүд", + "team_members_tab": "Ажлын талбарын гишүүд", + "teams": "Ажлын талбар", + "uid": "UID", + "unnamed": "(Нэргүй ажлын талбар)", + "viewer": "ҮЗЭГЧ", + "viewer_description": "Үзэгчид зөвхөн хүсэлтийг үзэж, ашиглах боломжтой", + "valid_name": "Хүчинтэй ажлын талбайн нэрийг оруулна уу", + "valid_owner_email": "Зөв эзэмшигчийн имэйл хаягаа оруулна уу" + }, + "users": { + "add_user": "Хэрэглэгч нэмэх", + "admin": "Админ", + "admin_id": "Админ ID", + "cancel": "Цуцлах", + "created_on": "Үүсгэсэн", + "copy_invite_link": "Урилгын холбоосыг хуулах", + "copy_link": "Холбоосыг хуулах", + "date": "Огноо", + "delete": "Устгах", + "delete_user": "Хэрэглэгчийг устгах", + "delete_users": "Хэрэглэгчдийг устгах", + "details": "Дэлгэрэнгүй мэдээлэл", + "edit": "Засварлах", + "email": "Имэйл", + "email_address": "Имэйл хаяг", + "empty_name": "Нэр хоосон байж болохгүй!!", + "id": "Хэрэглэгчийн ID", + "invalid_user": "Буруу хэрэглэгч", + "invite_load_list_error": "Уригдсан хэрэглэгчдийн жагсаалтыг ачаалах боломжгүй байна", + "invite_user": "Хэрэглэгчийг урих", + "invited_by": "Урьсан", + "invited_on": "Уригдсан", + "invited_users": "Уригдсан хэрэглэгчид", + "invitee_email": "Урьсан имэйл", + "last_active_on": "Сүүлийн идэвхтэй", + "load_info_error": "Хэрэглэгчийн мэдээллийг ачаалах боломжгүй байна", + "load_list_error": "Хэрэглэгчдийн жагсаалтыг ачаалах боломжгүй", + "make_admin": "Админ болго", + "name": "Нэр", + "new_user_added": "Шинэ хэрэглэгч нэмэгдсэн", + "no_invite": "Хүлээгдэж буй урилга олдсонгүй", + "no_invite_description": "Хүлээгдэж буй урилга байхгүй! Багийн найзуудаа Hoppscotch-д урьж эхлээрэй", + "no_shared_requests": "Хэрэглэгчийн үүсгэсэн хуваалцсан хүсэлт байхгүй байна", + "no_users": "Хэрэглэгч олдсонгүй", + "not_available": "Боломжгүй", + "not_found": "Хэрэглэгч олдсонгүй", + "pending_invites": "Хүлээгдэж буй урилга", + "remove_admin_privilege": "Админы эрхийг хасах", + "remove_admin_status": "Админ статусыг устгах", + "rename": "Нэрээ өөрчлөх", + "revoke_invitation": "Урилгыг хүчингүй болгох", + "searchbar_placeholder": "Нэр эсвэл имэйлээр хайх.", + "send_invite": "Урилга илгээх", + "show_more": "Илүү ихийг харуулах", + "uid": "UID", + "unnamed": "(Нэргүй хэрэглэгч)", + "user_not_found": "Хэрэглэгчийг инфра-д олоогүй байна!!", + "users": "Хэрэглэгчид", + "valid_email": "Хүчинтэй имэйл хаяг оруулна уу", + "deactivate": "Идэвхгүй болгох", + "deactivate_user": "Хэрэглэгчийг идэвхгүй болгох" + }, + "organization": { + "login_to_continue_description": "Байгууллагын жишээнд нэгдэхийн тулд та нэвтэрсэн байх шаардлагатай.", + "create_an_organization": "Байгууллага бий болгох", + "deactivate_user_failure": "Хэрэглэгчийг идэвхгүй болгож чадсангүй!!", + "deactivate_user_success": "Хэрэглэгчийг амжилттай идэвхгүй болголоо!!", + "delete_account_description": "Энэ нь таны Hoppscotch бүртгэлтэй холбоотой бүх өгөгдлийг устгах болно, үүнд энэ болон таны нэг хэсэг болох бусад байгууллага орно.", + "delete_account": "Hoppscotch дансыг устгах", + "user_deletion_failed_sole_admin": "Хэрэглэгч нь нэг буюу хэд хэдэн байгууллагын жишээнүүдийн цорын ганц админ юм. Устгах оролдлого хийхээсээ өмнө хэрэглэгчийн зэрэглэлийг бууруулна уу.", + "user_deletion_failed_sole_team_owner": "Хэрэглэгч нь нэг буюу хэд хэдэн байгууллагын тохиолдлын цорын ганц багийн эзэмшигч юм. Устгах оролдлого хийхээсээ өмнө өмчлөлийг шилжүүлэх эсвэл холбогдох ажлын талбарыг устгана уу.", + "no_organizations": "Та ямар ч байгууллагын гишүүн биш", + "admin": "Админ" + }, + "organization_sidebar": { + "hoppscotch_cloud": "Хоппскоч үүл", + "cloud_locked": "Өгөгдмөл жишээг устгах боломжгүй", + "admin": "Админ", + "error_loading": "Байгууллагуудыг ачаалж чадсангүй", + "inactive_orgs": "Идэвхгүй байгууллагууд", + "no_orgs_found": "Байгууллага олдсонгүй", + "no_active_orgs_found": "Идэвхтэй байгууллага байхгүй", + "organizations_for": "{email}-д зориулсан байгууллагууд", + "multi_account_notice": "Байгууллага бүр хамгийн сүүлд нэвтэрсэн акаунтыг ашиглан өөрийн нэвтрэх эрхээ хадгалдаг.", + "inactive_orgs_tooltip": "Тусламж авахын тулд дэмжлэгтэй холбоо барина уу." + }, + "billing": { + "confirm": { + "update_seat_count": "Та суудлын тоог {newSeatCount} болгож шинэчлэхдээ итгэлтэй байна уу?", + "update_billing_cycle": "Та төлбөрийн мөчлөгийг {newBillingCycle} болгон шинэчлэхдээ итгэлтэй байна уу?" + }, + "cancel_subscription": "Захиалга цуцлах" + }, + "app_console": { + "entries": "Консолын оруулгууд", + "no_entries": "Оруулга алга" + }, + "mockServer": { + "create_modal": { + "title": "Хуурамч сервер үүсгэх", + "name_label": "Хуурамч серверийн нэр", + "name_placeholder": "Хуурамч серверийн нэрийг оруулна уу", + "name_required": "Хуурамч серверийн нэр шаардлагатай", + "collection_source_label": "Цуглуулгын эх сурвалж", + "existing_collection": "Одоо байгаа цуглуулга", + "new_collection": "Шинэ цуглуулга", + "select_collection_label": "Цуглуулга сонгоно уу", + "select_collection_placeholder": "Цуглуулга сонгох", + "collection_required": "Цуглуулга сонгоно уу", + "collection_name_label": "Цуглуулгын нэр", + "collection_name_placeholder": "Цуглуулгын нэрийг оруулна уу", + "collection_name_required": "Цуглуулгын нэр шаардлагатай", + "request_config_label": "Хүсэлтийн тохиргоо", + "add_request": "Хүсэлт нэмэх", + "create_button": "Хуурамч сервер үүсгэх", + "success": "Хуурамч серверийг амжилттай үүсгэсэн", + "error": "Хуурамч сервер үүсгэж чадсангүй", + "no_collections": "Цуглуулга байхгүй байна" + }, + "edit_modal": { + "title": "Хуурамч серверийг засах", + "name_label": "Хуурамч серверийн нэр", + "name_placeholder": "Хуурамч серверийн нэрийг оруулна уу", + "name_required": "Хуурамч серверийн нэр шаардлагатай", + "active_label": "Идэвхтэй", + "url_label": "Хуурамч серверийн URL", + "collection_label": "Цуглуулга", + "update_button": "Хуурамч серверийг шинэчлэх", + "success": "Хуурамч серверийг амжилттай шинэчилсэн", + "error": "Хуурамч серверийг шинэчилж чадсангүй", + "url_copied": "URL-г санах ой руу хуулсан" + }, + "dashboard": { + "title": "Хуурамч серверүүд", + "subtitle": "Өөрийн API хуурамч серверүүдийг үүсгэж удирдах", + "create_button": "Хуурамч сервер үүсгэх", + "create_first": "Өөрийн анхны хуурамч серверийг үүсгэ", + "empty_title": "Хуурамч сервер олдсонгүй", + "empty_description": "Өөрийн API цуглуулгууд дээр тулгуурлан хуурамч серверүүдийг үүсгэн, урд талын хамааралгүйгээр фронт болон гар утасны хөгжүүлэлтийг идэвхжүүлнэ үү.", + "collection": "Цуглуулга", + "active": "Идэвхтэй", + "inactive": "Идэвхгүй", + "mock_url": "Хуурамч URL", + "endpoints": "Төгсгөлийн цэгүүд", + "created": "Үүсгэсэн", + "view_collection": "Цуглуулга үзэх", + "documentation": "Баримт бичиг", + "doc_description": "Энэ URL-г өөрийн аппликешнүүдэд API үндсэн URL болгон ашиглаарай:", + "url_copied": "Хуурамч серверийн URL-г санах ой руу хуулсан", + "delete_title": "Хуурамч серверийг устгах", + "delete_description": "Та энэ хуурамч серверийг устгахдаа итгэлтэй байна уу?", + "delete_success": "Хуурамч серверийг амжилттай устгалаа", + "delete_error": "Хуурамч серверийг устгаж чадсангүй" + } + } +} \ No newline at end of file diff --git a/packages/hoppscotch-common/package.json b/packages/hoppscotch-common/package.json index 0c836225fce..ea33bc16cad 100644 --- a/packages/hoppscotch-common/package.json +++ b/packages/hoppscotch-common/package.json @@ -1,7 +1,7 @@ { "name": "@hoppscotch/common", "private": true, - "version": "2026.4.1", + "version": "2026.5.0", "scripts": { "dev": "pnpm exec npm-run-all -p -l dev:*", "test": "vitest --run", @@ -40,7 +40,7 @@ "@hoppscotch/httpsnippet": "3.0.9", "@hoppscotch/js-sandbox": "workspace:^", "@hoppscotch/kernel": "workspace:^", - "@hoppscotch/plugin-appload": "github:CuriousCorrelation/tauri-plugin-appload#0d58d53be2bc75aeb5916bd0d77794fd209426af", + "@hoppscotch/plugin-appload": "github:CuriousCorrelation/tauri-plugin-appload#9d4528be4f385bccbe46859631d31aa2ee1ec0b6", "@hoppscotch/ui": "0.2.5", "@hoppscotch/vue-toasted": "0.1.0", "@lezer/highlight": "1.2.1", @@ -63,7 +63,7 @@ "buffer": "6.0.3", "cookie-es": "2.0.0", "dioc": "3.0.2", - "dompurify": "3.4.1", + "dompurify": "3.4.3", "esprima": "4.0.1", "events": "3.3.0", "fp-ts": "2.16.11", @@ -111,7 +111,7 @@ "util": "0.12.5", "uuid": "13.0.0", "verzod": "0.4.0", - "vue": "3.5.33", + "vue": "3.5.34", "vue-i18n": "11.4.0", "vue-json-pretty": "2.6.0", "vue-pdf-embed": "2.1.4", @@ -137,7 +137,7 @@ "@graphql-codegen/typescript-urql-graphcache": "3.1.1", "@graphql-codegen/urql-introspection": "3.0.1", "@graphql-typed-document-node/core": "3.2.0", - "@iconify-json/lucide": "1.2.104", + "@iconify-json/lucide": "1.2.107", "@import-meta-env/cli": "0.7.4", "@intlify/unplugin-vue-i18n": "11.1.2", "@relmify/jest-fp-ts": "2.1.1", @@ -148,15 +148,15 @@ "@types/nprogress": "0.2.3", "@types/paho-mqtt": "1.0.10", "@types/postman-collection": "3.5.11", - "@types/qs": "6.15.0", + "@types/qs": "6.15.1", "@types/splitpanes": "2.2.6", "@types/yargs-parser": "21.0.3", - "@typescript-eslint/eslint-plugin": "8.59.0", - "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", "@vitejs/plugin-vue": "6.0.6", - "@vue/compiler-sfc": "3.5.33", + "@vue/compiler-sfc": "3.5.34", "@vue/eslint-config-typescript": "14.7.0", - "@vue/runtime-core": "3.5.33", + "@vue/runtime-core": "3.5.34", "autoprefixer": "10.5.0", "cross-env": "10.1.0", "dotenv": "17.4.2", diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index 357e2240678..46b4f3976a9 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -78,6 +78,9 @@ declare module 'vue' { CollectionsEditRequest: typeof import('./components/collections/EditRequest.vue')['default'] CollectionsEditResponse: typeof import('./components/collections/EditResponse.vue')['default'] CollectionsExampleResponse: typeof import('./components/collections/ExampleResponse.vue')['default'] + CollectionsExportFormatList: typeof import('./components/collections/ExportFormatList.vue')['default'] + CollectionsExportFormatModal: typeof import('./components/collections/ExportFormatModal.vue')['default'] + CollectionsExportFormatStep: typeof import('./components/collections/ExportFormatStep.vue')['default'] CollectionsGraphql: typeof import('./components/collections/graphql/index.vue')['default'] CollectionsGraphqlAdd: typeof import('./components/collections/graphql/Add.vue')['default'] CollectionsGraphqlAddFolder: typeof import('./components/collections/graphql/AddFolder.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/collections/Collection.vue b/packages/hoppscotch-common/src/components/collections/Collection.vue index 2a9af3baa88..08c345d69dc 100644 --- a/packages/hoppscotch-common/src/components/collections/Collection.vue +++ b/packages/hoppscotch-common/src/components/collections/Collection.vue @@ -263,17 +263,16 @@ v-if="!hasNoTeamAccess" ref="exportAction" :icon="IconDownload" - :label="t('export.title')" + :label="t('export.collection')" :shortcut="['X']" :loading="exportLoading" @click=" () => { - ;(emit('export-data'), - collectionsType === 'my-collections' ? hide() : null) + emit('export-data') + hide() } " /> - +
+
+ + + +
+ +
+ + + diff --git a/packages/hoppscotch-common/src/components/collections/ExportFormatModal.vue b/packages/hoppscotch-common/src/components/collections/ExportFormatModal.vue new file mode 100644 index 00000000000..b6c0d25c4ca --- /dev/null +++ b/packages/hoppscotch-common/src/components/collections/ExportFormatModal.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/hoppscotch-common/src/components/collections/ExportFormatStep.vue b/packages/hoppscotch-common/src/components/collections/ExportFormatStep.vue new file mode 100644 index 00000000000..6d0020209b0 --- /dev/null +++ b/packages/hoppscotch-common/src/components/collections/ExportFormatStep.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/hoppscotch-common/src/components/collections/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/ImportExport.vue index 485d2bc5fb8..4572a7f2d44 100644 --- a/packages/hoppscotch-common/src/components/collections/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/collections/ImportExport.vue @@ -4,7 +4,9 @@ modal-title="modal.collections" :importer-modules="importerModules" :exporter-modules="exporterModules" - :has-team-write-access="hasTeamWriteAccess" + :has-team-write-access=" + hasTeamWriteAccess || collectionsType.type === 'my-collections' + " @hide-modal="emit('hide-modal')" /> @@ -18,7 +20,9 @@ import { transformCollectionForImport } from "~/helpers/collection/collection" import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource" import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource" +import IconDownload from "~icons/lucide/download" import IconFile from "~icons/lucide/file" +import yaml from "js-yaml" import { harImporter, @@ -32,9 +36,16 @@ import { import { defineStep } from "~/composables/step-components" import AllCollectionImport from "~/components/importExport/ImportExportSteps/AllCollectionImport.vue" +import CollectionsExportFormatStep from "~/components/collections/ExportFormatStep.vue" import { useI18n } from "~/composables/i18n" import { useToast } from "~/composables/toast" import { appendRESTCollections, restCollections$ } from "~/newstore/collections" +import { + ensureRefIds, + flushUnmatchedRefIdsFromTree, + populateLocalStoresFromCollectionTree, + stripCollectionTreeForStore, +} from "~/helpers/secretVariables" import IconInsomnia from "~icons/hopp/insomnia" import IconPostman from "~icons/hopp/postman" @@ -50,9 +61,13 @@ import { getTeamCollectionJSON } from "~/helpers/backend/helpers" import { platform } from "~/platform" -import { initializeDownloadFile } from "~/helpers/import-export/export" +import { + initializeDownloadFile, + stripRefIdReplacer, +} from "~/helpers/import-export/export" import { gistExporter } from "~/helpers/import-export/export/gist" import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections" +import { hoppCollectionsToOpenAPI } from "~/helpers/import-export/export/openapi" import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections" import { ImporterOrExporter } from "~/components/importExport/types" @@ -121,7 +136,11 @@ const handleImportToStore = async (collections: HoppCollection[]) => { */ const importToPersonalWorkspace = (collections: HoppCollection[]) => { // Remove old id from the imported collection and folders and transform it to new collection format - const sanitizedCollections = collections.map(sanitizeCollection) + const sanitizedCollections = collections + .map(sanitizeCollection) + .map(ensureRefIds) + + sanitizedCollections.forEach(populateLocalStoresFromCollectionTree) if ( platform.sync.collections.importToPersonalWorkspace && @@ -134,14 +153,17 @@ const importToPersonalWorkspace = (collections: HoppCollection[]) => { ) } - appendRESTCollections(sanitizedCollections) + appendRESTCollections(sanitizedCollections.map(stripCollectionTreeForStore)) return E.right({ success: true }) } /** - * Import collections to teams workspace - * No need to sanitize the collections before importing to teams workspace because the BE handles this and add the new id to the collection and folders - * @param collections Collections to import + * Import collections to teams workspace. Stamps `_ref_id` and seeds the + * device-local secret stores under it; on the team-collection-added + * subscription, `TeamCollectionsService.addCollection` migrates entries + * from `_ref_id` to the backend-assigned `id`. Wire payload is stripped + * of secrets via `transformCollectionForImport`; secrets stay + * device-local per the team-isolation model. */ const importToTeamsWorkspace = async (collections: HoppCollection[]) => { if (!hasTeamWriteAccess.value || !selectedTeamID.value) { @@ -150,7 +172,10 @@ const importToTeamsWorkspace = async (collections: HoppCollection[]) => { }) } - const transformedCollection = collections.map((collection) => + const collectionsWithRefIds = collections.map(ensureRefIds) + collectionsWithRefIds.forEach(populateLocalStoresFromCollectionTree) + + const transformedCollection = collectionsWithRefIds.map((collection) => transformCollectionForImport(collection) ) @@ -159,19 +184,23 @@ const importToTeamsWorkspace = async (collections: HoppCollection[]) => { selectedTeamID.value )() - return E.isRight(res) - ? E.right({ success: true }) - : E.left({ - success: false, - }) + if (E.isLeft(res)) { + // Backend rejected — flush ONLY `_ref_id`-keyed entries we just + // seeded. `flushLocalStoresForCollectionTree` would also delete by + // `node.id`, which could be a live backend id from a same-workspace + // re-import and would wipe existing collections' in-memory secrets. + // Empty `keptRefIds` ⇒ every `_ref_id` in the tree is flushed. + flushUnmatchedRefIdsFromTree(collectionsWithRefIds, new Set()) + return E.left({ success: false }) + } + return E.right({ success: true }) } const emit = defineEmits<{ (e: "hide-modal"): () => void }>() -const isHoppMyCollectionExporterInProgress = ref(false) -const isHoppTeamCollectionExporterInProgress = ref(false) +const isWorkspaceExportInProgress = ref(false) const isHoppGistCollectionExporterInProgress = ref(false) const isPostmanImporterInProgress = ref(false) @@ -569,89 +598,171 @@ const HoppGistImporter: ImporterOrExporter = { }), } -const HoppMyCollectionsExporter: ImporterOrExporter = { - metadata: { - id: "hopp_my_collections", - name: "export.as_json", - title: "action.download_file", - icon: IconUser, - disabled: false, - applicableTo: ["personal-workspace"], - isLoading: isHoppMyCollectionExporterInProgress, - format: "hoppscotch", - }, - importSummary: currentImportSummary, - action: async () => { +/** + * Resolve the array of HoppCollections for the current workspace. For the + * team workspace we have to fetch + parse the GraphQL JSON; for personal it's + * already a reactive ref. + */ +const getWorkspaceCollections = async (): Promise<{ + collections: HoppCollection[] + workspaceName: string + fileBaseName: string +} | null> => { + if (props.collectionsType.type === "my-collections") { if (!myCollections.value.length) { - return toast.error(t("error.no_collections_to_export")) + toast.error(t("error.no_collections_to_export")) + return null } + return { + collections: myCollections.value, + workspaceName: "Hoppscotch personal collections", + fileBaseName: "hoppscotch-personal-collections", + } + } - isHoppMyCollectionExporterInProgress.value = true + if (!props.collectionsType.selectedTeam) return null + const team = props.collectionsType.selectedTeam + const res = await teamCollectionsExporter(team.teamID) + if (E.isLeft(res)) { + toast.error(res.left) + return null + } - const message = await initializeDownloadFile( - myCollectionsExporter(myCollections.value), - "hoppscotch-personal-collections" - ) + let collections: HoppCollection[] + try { + collections = JSON.parse(res.right) as HoppCollection[] + } catch { + toast.error(t("error.something_went_wrong")) + return null + } + if (!collections.length) { + toast.error(t("error.no_collections_to_export")) + return null + } - if (E.isRight(message)) { - toast.success(t("state.download_started")) + return { + collections, + workspaceName: team.teamName ?? "Hoppscotch team", + fileBaseName: "hoppscotch-team-collections", + } +} +const onWorkspaceExportHopp = async () => { + // Keep the modal open during the GraphQL fetch (team workspaces) and + // serialization. Closing early hid that latency from the user. + isWorkspaceExportInProgress.value = true + let saved = false + try { + const ctx = await getWorkspaceCollections() + if (!ctx) return + + const json = + props.collectionsType.type === "my-collections" + ? myCollectionsExporter(ctx.collections) + : JSON.stringify(ctx.collections, null, 2) + + const message = await initializeDownloadFile(json, ctx.fileBaseName) + if (E.isRight(message)) { + toast.success(t(message.right)) platform.analytics?.logEvent({ type: "HOPP_EXPORT_COLLECTION", exporter: "json", platform: "rest", }) + saved = true } else { toast.error(t(message.left)) } - - isHoppMyCollectionExporterInProgress.value = false - }, + } catch { + toast.error(t("export.failed")) + } finally { + isWorkspaceExportInProgress.value = false + // Only close the parent modal when the file actually saved. On cancel, + // no-data, or error the user stays on the format chooser so toasts and + // state remain visible and they can retry. + if (saved) emit("hide-modal") + } } -const HoppTeamCollectionsExporter: ImporterOrExporter = { - metadata: { - id: "hopp_team_collections", - name: "export.as_json", - title: "export.as_json", - icon: IconUser, - disabled: false, - applicableTo: ["team-workspace"], - isLoading: isHoppTeamCollectionExporterInProgress, - }, - importSummary: currentImportSummary, - action: async () => { - isHoppTeamCollectionExporterInProgress.value = true - if ( - props.collectionsType.type === "team-collections" && - props.collectionsType.selectedTeam - ) { - const res = await teamCollectionsExporter( - props.collectionsType.selectedTeam.teamID - ) - - if (E.isRight(res)) { - const message = await initializeDownloadFile( - res.right, - "hoppscotch-team-collections" - ) +const onWorkspaceExportOpenAPI = async (format: "json" | "yaml") => { + isWorkspaceExportInProgress.value = true + let saved = false + try { + const ctx = await getWorkspaceCollections() + if (!ctx) return - E.isRight(message) - ? toast.success(t(message.right)) - : toast.error(t(message.left)) + // The chooser modal already shows the full list of OpenAPI lossiness + // categories upfront, so the converter's post-export warnings would be + // redundant — drop them. + const { doc } = hoppCollectionsToOpenAPI(ctx.workspaceName, ctx.collections) - platform.analytics?.logEvent({ - type: "HOPP_EXPORT_COLLECTION", - exporter: "json", - platform: "rest", - }) - } else { - toast.error(res.left) - } + const isYaml = format === "yaml" + let data: string + try { + data = isYaml ? yaml.dump(doc) : JSON.stringify(doc, null, 2) + } catch { + toast.error(t("error.something_went_wrong")) + return } + const extension = isYaml ? "yaml" : "json" + const result = await platform.kernelIO.saveFileWithDialog({ + data, + contentType: isYaml ? "application/x-yaml" : "application/json", + suggestedFilename: `${ctx.fileBaseName}-openapi.${extension}`, + filters: [ + { + name: `OpenAPI ${extension.toUpperCase()} file`, + extensions: [extension], + }, + ], + }) + if (result.type === "saved" || result.type === "unknown") { + toast.success(t("state.download_started").toString()) + platform.analytics?.logEvent({ + type: "HOPP_EXPORT_COLLECTION", + exporter: "openapi", + platform: "rest", + }) + saved = true + } + } catch { + toast.error(t("export.failed")) + } finally { + isWorkspaceExportInProgress.value = false + if (saved) emit("hide-modal") + } +} - isHoppTeamCollectionExporterInProgress.value = false +// In-modal step that renders the format chooser (Hoppscotch JSON / OpenAPI +// JSON / OpenAPI YAML + lossiness disclosure). Registered as the exporter's +// `component` so ImportExportBase advances to it on click instead of opening +// a parallel modal. +const exportFormatStep = defineStep( + "collections_export_format", + CollectionsExportFormatStep, + () => ({ + loading: isWorkspaceExportInProgress.value, + "onExport-hoppscotch": () => { + onWorkspaceExportHopp() + }, + "onExport-openapi": (format: "json" | "yaml") => { + onWorkspaceExportOpenAPI(format) + }, + }) +) + +const HoppCollectionsExporter: ImporterOrExporter = { + metadata: { + id: "hopp_collections_export", + name: "export.collections", + title: "export.choose_format", + icon: IconDownload, + disabled: false, + applicableTo: ["personal-workspace", "team-workspace"], + isLoading: isWorkspaceExportInProgress, }, + importSummary: currentImportSummary, + component: exportFormatStep, } const HoppGistCollectionsExporter: ImporterOrExporter = { @@ -777,10 +888,7 @@ const importerModules = computed(() => { }) const exporterModules = computed(() => { - const enabledExporters = [ - HoppMyCollectionsExporter, - HoppTeamCollectionsExporter, - ] + const enabledExporters = [HoppCollectionsExporter] if (platform.platformFeatureFlags.exportAsGIST) { enabledExporters.push(HoppGistCollectionsExporter) @@ -831,7 +939,8 @@ const getCollectionJSON = async () => { } if (props.collectionsType.type === "my-collections") { - return E.right(JSON.stringify(myCollections.value, null, 2)) + const stripped = myCollections.value.map(stripCollectionTreeForStore) + return E.right(JSON.stringify(stripped, stripRefIdReplacer, 2)) } return E.left("INVALID_SELECTED_TEAM_OR_INVALID_COLLECTION_TYPE") diff --git a/packages/hoppscotch-common/src/components/collections/documentation/index.vue b/packages/hoppscotch-common/src/components/collections/documentation/index.vue index c82ecbb1f05..6100f360f6e 100644 --- a/packages/hoppscotch-common/src/components/collections/documentation/index.vue +++ b/packages/hoppscotch-common/src/components/collections/documentation/index.vue @@ -270,6 +270,7 @@ import { import { updateTeamCollection } from "~/helpers/backend/mutations/TeamCollection" import { updateTeamRequest } from "~/helpers/backend/mutations/TeamRequest" +import { stripSecretVariableValuesForWire } from "~/helpers/secretVariables" import { CollectionDataProps, getSingleTeamCollectionJSON, @@ -735,7 +736,7 @@ const saveCollectionDocumentation = async () => { const data: CollectionDataProps = { auth: collection.auth || { authType: "inherit", authActive: true }, headers: collection.headers || [], - variables: collection.variables || [], + variables: stripSecretVariableValuesForWire(collection.variables || []), description: documentationDescription.value, preRequestScript: collection.preRequestScript || "", testScript: collection.testScript || "", @@ -831,7 +832,9 @@ const saveCollectionDocumentationById = async ( const data: CollectionDataProps = { auth: collectionData.auth || { authType: "inherit", authActive: true }, headers: collectionData.headers || [], - variables: collectionData.variables || [], + variables: stripSecretVariableValuesForWire( + collectionData.variables || [] + ), description: documentation, preRequestScript: collectionData.preRequestScript || "", testScript: collectionData.testScript || "", diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue b/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue index df4d38b3672..14803af4914 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue @@ -250,6 +250,7 @@ import { useService } from "dioc/vue" import { computed, ref } from "vue" import { Picked } from "~/helpers/types/HoppPicked" import { removeGraphqlCollection } from "~/newstore/collections" +import { flushLocalStoresForCollectionTree } from "~/helpers/secretVariables" import { handleTokenValidation } from "~/helpers/handleTokenValidation" import { GQLTabService } from "~/services/tab/graphql" import IconCheckCircle from "~icons/lucide/check-circle" @@ -382,6 +383,8 @@ const removeCollection = async () => { tab.value.document.isDirty = true } + flushLocalStoresForCollectionTree(props.collection) + removeGraphqlCollection(props.collectionIndex, props.collection.id) toast.success(`${t("state.deleted")}`) } diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue b/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue index a2cb4e381ac..2b710b144f2 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue @@ -235,6 +235,7 @@ import { computed, ref } from "vue" import { handleTokenValidation } from "~/helpers/handleTokenValidation" import { Picked } from "~/helpers/types/HoppPicked" import { removeGraphqlFolder } from "~/newstore/collections" +import { flushLocalStoresForCollectionTree } from "~/helpers/secretVariables" import { GQLTabService } from "~/services/tab/graphql" import IconCheckCircle from "~icons/lucide/check-circle" import IconCopy from "~icons/lucide/copy" @@ -345,6 +346,8 @@ const removeFolder = async () => { tab.value.document.isDirty = true } + flushLocalStoresForCollectionTree(props.folder) + removeGraphqlFolder(props.folderPath, props.folder.id) toast.success(t("state.deleted")) } diff --git a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue index 08f8570422d..77ebdf16c42 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue @@ -35,6 +35,11 @@ import { gistExporter } from "~/helpers/import-export/export/gist" import { computed } from "vue" import { hoppGQLImporter } from "~/helpers/import-export/import/hopp" import { ReqType } from "~/helpers/backend/graphql" +import { + ensureRefIds, + populateLocalStoresFromCollectionTree, + stripCollectionTreeForStore, +} from "~/helpers/secretVariables" const t = useI18n() const toast = useToast() @@ -191,10 +196,8 @@ const GqlCollectionsGistExporter: ImporterOrExporter = { const accessToken = currentUser.value?.accessToken if (accessToken) { - const res = await gistExporter( - JSON.stringify(gqlCollections.value), - accessToken - ) + const stripped = gqlCollections.value.map(stripCollectionTreeForStore) + const res = await gistExporter(JSON.stringify(stripped), accessToken) if (E.isLeft(res)) { toast.error(t("export.failed")) @@ -233,17 +236,22 @@ const showImportFailedError = () => { } const handleImportToStore = (gqlCollections: HoppCollection[]) => { + const collectionsWithRefIds = gqlCollections.map(ensureRefIds) + collectionsWithRefIds.forEach(populateLocalStoresFromCollectionTree) + if ( platform.sync.collections.importToPersonalWorkspace && currentUser.value ) { return platform.sync.collections.importToPersonalWorkspace( - gqlCollections, + collectionsWithRefIds, ReqType.Gql ) } - appendGraphqlCollections(gqlCollections) + appendGraphqlCollections( + collectionsWithRefIds.map(stripCollectionTreeForStore) + ) toast.success(t("state.file_imported")) } diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue index f9387c91fbe..0c731018d9e 100644 --- a/packages/hoppscotch-common/src/components/collections/index.vue +++ b/packages/hoppscotch-common/src/components/collections/index.vue @@ -225,6 +225,13 @@ + (null) const editingResponseID = ref(null) const showAddExampleModal = ref(false) +// The queued collection is held here so async export work can read it after +// the modal closes. +const showExportModal = ref(false) +const exportTargetCollection = ref(null) const editingProperties = ref({ collection: null, @@ -886,6 +905,38 @@ const displayModalAddExample = (show: boolean) => { if (!show) resetSelectedData() } +let exportGeneration = 0 + +const closeExportModal = () => { + // Bump so any in-flight export's `finally` no-ops instead of closing a + // freshly-opened modal after this dismissal. + exportGeneration++ + showExportModal.value = false + exportTargetCollection.value = null + exportLoading.value = false +} + +const onExportHopp = async () => { + const collection = exportTargetCollection.value + if (!collection) return + // `doExportHoppCollection` sets `exportLoading = true` on entry; the reset + // is owned here via the generation-guarded `closeExportModal()` so a stale + // in-flight export cannot re-enable buttons on a freshly-opened modal. + // Same `exportGeneration` pattern as `doExportOpenAPI`. + const thisGeneration = ++exportGeneration + try { + await doExportHoppCollection(collection) + } finally { + if (thisGeneration === exportGeneration) closeExportModal() + } +} + +const onExportOpenAPI = (format: "json" | "yaml") => { + // Modal stays open through the export to keep the loading state visible; + // doExportOpenAPI closes it when finished. + doExportOpenAPI(format) +} + const addNewRootCollection = async (name: string) => { if (collectionsType.value.type === "my-collections") { modalLoadingState.value = true @@ -1992,15 +2043,8 @@ const onRemoveCollection = async () => { toast.success(t("state.deleted")) displayConfirmModal(false) - // delete the secret collection variables - // and current collection variables value if the collection is removed if (collectionToRemove) { - secretEnvironmentService.deleteSecretEnvironment( - collectionToRemove._ref_id ?? `${collectionIndex}` - ) - currentEnvironmentValueService.deleteEnvironment( - collectionToRemove._ref_id ?? `${collectionIndex}` - ) + flushLocalStoresForCollectionTree(collectionToRemove) } } else if (hasTeamWriteAccess.value) { const collectionID = editingCollectionID.value @@ -2015,12 +2059,20 @@ const onRemoveCollection = async () => { emit("select", null) } + // Capture the subtree BEFORE the mutation so the + // team-collection-removed subscription can't drop it from FE state + // before we flush nested entries. + const subtreeSnapshot = + teamCollectionService.findCollectionByID(collectionID) + removeTeamCollectionOrFolder(collectionID).then(() => { resetTeamRequestsContext() - // delete the secret collection variables - // and current collection variables value if the collection is removed - if (collectionID) { + if (subtreeSnapshot) { + flushLocalStoresForTeamCollectionTree(subtreeSnapshot) + } else if (collectionID) { + // Snapshot miss (already removed from tree). Flush at least the + // top-level id so the secret service doesn't leak this entry. secretEnvironmentService.deleteSecretEnvironment(collectionID) currentEnvironmentValueService.deleteEnvironment(collectionID) } @@ -2053,8 +2105,6 @@ const onRemoveFolder = async () => { emit("select", null) } - const folderIndex = pathToLastIndex(folderPath) - const folderToRemove = folderPath ? navigateToFolderWithIndexPath( restCollectionStore.value.state, @@ -2075,15 +2125,8 @@ const onRemoveFolder = async () => { toast.success(t("state.deleted")) displayConfirmModal(false) - // delete the secret collection variables - // and current collection variables value if the collection is removed if (folderToRemove) { - secretEnvironmentService.deleteSecretEnvironment( - folderToRemove.id ?? `${folderIndex}` - ) - currentEnvironmentValueService.deleteEnvironment( - folderToRemove.id ?? `${folderIndex}` - ) + flushLocalStoresForCollectionTree(folderToRemove) } } else if (hasTeamWriteAccess.value) { const collectionID = editingCollectionID.value @@ -2098,12 +2141,15 @@ const onRemoveFolder = async () => { emit("select", null) } + const subtreeSnapshot = + teamCollectionService.findCollectionByID(collectionID) + removeTeamCollectionOrFolder(collectionID).then(() => { resetTeamRequestsContext() - // delete the secret collection variables - // and current collection variables value if the collection is removed - if (collectionID) { + if (subtreeSnapshot) { + flushLocalStoresForTeamCollectionTree(subtreeSnapshot) + } else if (collectionID) { secretEnvironmentService.deleteSecretEnvironment(collectionID) currentEnvironmentValueService.deleteEnvironment(collectionID) } @@ -3145,57 +3191,164 @@ const initializeDownloadCollection = async ( if (result.type === "unknown" || result.type === "saved") { toast.success(t("state.download_started").toString()) + platform.analytics?.logEvent({ + type: "HOPP_EXPORT_COLLECTION", + exporter: "json", + platform: "rest", + }) } } /** - * Export a specific collection or folder - * Triggered by the export button in the tippy menu - * @param collection - Collection or folder to be exported + * Entry point from the collection context menu. Opens the format-chooser + * modal so the user can pick between Hoppscotch JSON and OpenAPI 3.1. */ -const exportData = async (collection: HoppCollection | TeamCollection) => { - if (collectionsType.value.type === "my-collections") { - const collectionJSON = JSON.stringify(collection, stripRefIdReplacer, 2) +const exportData = (collection: HoppCollection | TeamCollection) => { + exportTargetCollection.value = collection + showExportModal.value = true +} - // Strip `export {};\n` from `testScript` and `preRequestScript` fields - const cleanedCollectionJSON = - stripJsonSerializedModulePrefix(collectionJSON) +/** + * Native Hoppscotch JSON export (the original behavior of `exportData`). + */ +const doExportHoppCollection = async ( + collection: HoppCollection | TeamCollection +) => { + // Strip `export {};\n` from `testScript` and `preRequestScript` fields, and + // strip `_ref_id` / secret variable values via `stripCollectionTreeForStore` + // so neither leaks into the exported file. + const stringifyForExport = (coll: HoppCollection): string => + stripJsonSerializedModulePrefix( + JSON.stringify(stripCollectionTreeForStore(coll), stripRefIdReplacer, 2) + ) - const name = (collection as HoppCollection).name + exportLoading.value = true + try { + if (collectionsType.value.type === "my-collections") { + const hoppColl = collection as HoppCollection + await initializeDownloadCollection( + stringifyForExport(hoppColl), + hoppColl.name + ) + return + } - initializeDownloadCollection(cleanedCollectionJSON, name) - } else { if (!collection.id) return - exportLoading.value = true + const teamID = collectionsType.value.selectedTeam?.teamID + if (!teamID) return + + const result = await getTeamCollectionObject(teamID, collection.id) + if (E.isLeft(result)) { + const errMsg = + typeof result.left === "string" + ? result.left + : getErrorMessage(result.left) + toast.error(`${errMsg}`) + return + } + const hoppColl = result.right + await initializeDownloadCollection( + stringifyForExport(hoppColl), + hoppColl.name + ) + } catch { + toast.error(t("error.something_went_wrong")) + } +} - pipe( - getCompleteCollectionTree(collection.id), - TE.match( - (err: GQLError) => { - toast.error(`${getErrorMessage(err)}`) - exportLoading.value = false - return +const doExportOpenAPI = async (format: "json" | "yaml") => { + const collection = exportTargetCollection.value + if (!collection) return + + const thisGeneration = ++exportGeneration + + const saveOpenAPIDoc = async ( + openAPIDoc: Record, + name: string + ) => { + const isYaml = format === "yaml" + let data: string + try { + data = isYaml + ? yaml.dump(openAPIDoc) + : JSON.stringify(openAPIDoc, null, 2) + } catch { + toast.error(t("error.something_went_wrong")) + return + } + const contentType = isYaml ? "application/x-yaml" : "application/json" + const extension = isYaml ? "yaml" : "json" + + // `saveFileWithDialog` returns a discriminated SaveFileResponse and does + // not throw — checking `result.type` is the only correct way to know if + // the user actually saved or cancelled. + const result = await platform.kernelIO.saveFileWithDialog({ + data, + contentType, + suggestedFilename: `${name}-openapi.${extension}`, + filters: [ + { + name: `OpenAPI ${extension.toUpperCase()} file`, + extensions: [extension], }, - async (coll) => { - const hoppColl = teamCollToHoppRESTColl(coll) - const collectionJSONString = JSON.stringify( - hoppColl, - stripRefIdReplacer, - 2 - ) + ], + }) - // Strip `export {};\n` from `testScript` and `preRequestScript` fields - const cleanedCollectionJSON = - stripJsonSerializedModulePrefix(collectionJSONString) + if (result.type === "saved" || result.type === "unknown") { + toast.success(t("state.download_started").toString()) + platform.analytics?.logEvent({ + type: "HOPP_EXPORT_COLLECTION", + exporter: "openapi", + platform: "rest", + }) + } + } - await initializeDownloadCollection( - cleanedCollectionJSON, - hoppColl.name - ) - exportLoading.value = false - } + exportLoading.value = true + + if (collectionsType.value.type === "my-collections") { + try { + // Chooser modal warns about lossiness upfront; per-export warnings would + // be redundant. + const { doc: openAPIDoc } = hoppCollectionToOpenAPI( + collection as HoppCollection ) - )() + const name = (collection as HoppCollection).name + await saveOpenAPIDoc(openAPIDoc, name) + } catch { + toast.error(t("error.something_went_wrong")) + } finally { + if (thisGeneration === exportGeneration) closeExportModal() + } + } else { + if (!collection.id) { + closeExportModal() + return + } + const teamID = collectionsType.value.selectedTeam?.teamID + if (!teamID) { + closeExportModal() + return + } + + try { + const result = await getTeamCollectionObject(teamID, collection.id) + if (E.isLeft(result)) { + const errMsg = + typeof result.left === "string" + ? result.left + : getErrorMessage(result.left) + toast.error(`${errMsg}`) + return + } + const hoppColl = result.right + const { doc: openAPIDoc } = hoppCollectionToOpenAPI(hoppColl) + await saveOpenAPIDoc(openAPIDoc, hoppColl.name) + } catch { + toast.error(t("error.something_went_wrong")) + } finally { + if (thisGeneration === exportGeneration) closeExportModal() + } } } @@ -3235,6 +3388,26 @@ const getCurrentValue = ( )?.currentValue } +/** + * Restore both `initialValue` and `currentValue` for a secret variable from + * the local secret store. Both fields are blanked at the wire boundary + * before the variable is sent to the backend, so when the user reopens the + * Properties modal we re-populate from `secretEnvironmentService`. + * Returns null for non-secret variables (callers fall back to existing + * current-value lookup) or when the slot has no entry in the secret store. + */ +const getSecretValues = ( + isSecret: boolean, + varIndex: number, + collectionID: string +): { value: string; initialValue: string } | null => { + if (!isSecret) return null + return secretEnvironmentService.getSecretEnvironmentVariableValue( + collectionID, + varIndex + ) +} + const editProperties = async (payload: { collectionIndex: string collection: HoppCollection | TeamCollection @@ -3272,14 +3445,15 @@ const editProperties = async (payload: { const collectionVariables = pipe( (collection as HoppCollection).variables ?? [], A.mapWithIndex((index, e) => { + const storeID = (collection as HoppCollection)._ref_id ?? collectionId! + const stored = getSecretValues(e.secret, index, storeID) return { ...e, currentValue: - getCurrentValue( - e.secret, - index, - (collection as HoppCollection)._ref_id ?? collectionId! - ) ?? e.currentValue, + stored?.value ?? + getCurrentValue(e.secret, index, storeID) ?? + e.currentValue, + initialValue: stored?.initialValue ?? e.initialValue, } }) ) @@ -3333,10 +3507,14 @@ const editProperties = async (payload: { const collectionVariables = pipe( (data.variables ?? []) as HoppCollectionVariable[], A.mapWithIndex((index, e) => { + const stored = getSecretValues(e.secret, index, collectionId!) return { ...e, currentValue: - getCurrentValue(e.secret, index, collectionId!) ?? e.currentValue, + stored?.value ?? + getCurrentValue(e.secret, index, collectionId!) ?? + e.currentValue, + initialValue: stored?.initialValue ?? e.initialValue, } }) ) @@ -3399,6 +3577,7 @@ const setCollectionProperties = (newCollection: { ? O.some({ key: e.key, value: e.currentValue, + initialValue: e.initialValue, varIndex: i, }) : O.none @@ -3419,24 +3598,17 @@ const setCollectionProperties = (newCollection: { ) ) - secretEnvironmentService.addSecretEnvironment( - collection._ref_id ?? collectionId!, - secretVariables - ) + // Mirror the read-side keying in `editProperties`. + const storeKey = + collectionsType.value.type === "team-collections" + ? collectionId! + : (collection._ref_id ?? collectionId!) - currentEnvironmentValueService.addEnvironment( - collection._ref_id ?? collectionId!, - nonSecretVariables - ) + secretEnvironmentService.addSecretEnvironment(storeKey, secretVariables) - //set current value and secret values to empty string - collection.variables = pipe( - filteredVariables, - A.map((e) => ({ - ...e, - currentValue: "", - })) - ) + currentEnvironmentValueService.addEnvironment(storeKey, nonSecretVariables) + + collection.variables = stripSecretVariableValuesForWire(filteredVariables) } if (collectionsType.value.type === "my-collections") { @@ -3451,7 +3623,7 @@ const setCollectionProperties = (newCollection: { }) toast.success(t("collection.properties_updated")) } else if (hasTeamWriteAccess.value && collectionId) { - const data = { + const data: CollectionDataProps = { auth: collection.auth ?? { authType: "inherit", authActive: true, diff --git a/packages/hoppscotch-common/src/components/environments/Add.vue b/packages/hoppscotch-common/src/components/environments/Add.vue index 71dedb1da11..1a9ec22c80d 100644 --- a/packages/hoppscotch-common/src/components/environments/Add.vue +++ b/packages/hoppscotch-common/src/components/environments/Add.vue @@ -79,6 +79,7 @@ import { useToast } from "~/composables/toast" import { GQLError } from "~/helpers/backend/GQLClient" import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment" import { getEnvActionErrorMessage } from "~/helpers/error-messages" +import { stripSecretVariableValuesForWire } from "~/helpers/secretVariables" import { setGlobalEnvVariables, updateEnvironment, @@ -205,7 +206,7 @@ const addEnvironment = async () => { await pipe( updateTeamEnvironment( - JSON.stringify(newVariables), + JSON.stringify(stripSecretVariableValuesForWire(newVariables)), scope.value.environment.id, scope.value.environment.environment.name ), diff --git a/packages/hoppscotch-common/src/components/environments/ImportExport.vue b/packages/hoppscotch-common/src/components/environments/ImportExport.vue index 7db641fa224..b1a168d5365 100644 --- a/packages/hoppscotch-common/src/components/environments/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/environments/ImportExport.vue @@ -9,7 +9,12 @@