Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11,684 changes: 7,291 additions & 4,393 deletions package-lock.json

Large diffs are not rendered by default.

29 changes: 13 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,20 @@
"verify": "npm run format:check && npm run lint && npm run test:cov"
},
"devDependencies": {
"@apollo/client": "^3.6.9",
"@apollo/client": "^3.13.8",
"@docusaurus/core": "^2.4.3",
"@docusaurus/module-type-aliases": "^2.4.3",
"@docusaurus/preset-classic": "^2.4.3",
"@grpc/grpc-js": "^1.6.7",
"@grpc/proto-loader": "^0.7.4",
"@nestjs/apollo": "^10.1.0",
"@nestjs/common": "^9.0.11",
"@nestjs/core": "^9.0.11",
"@nestjs/graphql": "^10.1.1",
"@nestjs/microservices": "^9.0.11",
"@nestjs/platform-express": "^9.0.11",
"@nestjs/platform-fastify": "^9.0.11",
"@nestjs/testing": "^9.0.11",
"@nestjs/apollo": "^13.1.0",
"@nestjs/common": "^11.1.3",
"@nestjs/core": "^11.1.3",
"@nestjs/graphql": "^13.1.0",
"@nestjs/microservices": "^11.1.3",
"@nestjs/platform-express": "^11.1.3",
"@nestjs/platform-fastify": "^11.1.3",
"@nestjs/testing": "^11.1.3",
"@tsconfig/docusaurus": "^2.0.1",
"@types/accept-language-parser": "^1.5.3",
"@types/cookie": "^0.5.2",
Expand All @@ -82,12 +82,9 @@
"@types/supertest": "^2.0.12",
"@types/validator": "^13.11.1",
"@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^6.7.2",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"apollo-link-ws": "^1.0.20",
"apollo-server-core": "^3.9.0",
"apollo-server-express": "^3.9.0",
"@typescript-eslint/parser": "^6.7.2",
"apollo-server-core": "^3.13.0",
"apollo-server-express": "^3.13.0",
"bumpp": "^9.2.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
Expand All @@ -96,7 +93,7 @@
"eslint": "^8.49.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"graphql-subscriptions": "^2.0.0",
"graphql-subscriptions": "^3.0.0",
"graphql-tag": "^2.12.6",
"hbs": "^4.2.0",
"jest": "^29.7.0",
Expand Down
4 changes: 2 additions & 2 deletions src/services/i18n.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,15 @@ export class I18nService<K = Record<string, unknown>>
let offset = 0;
for (const nestedTranslation of nestedTranslations) {
const result = rootTranslations
? (this.translateObject(
? ((this.translateObject(
nestedTranslation.key,
rootTranslations,
lang,
{
...options,
args: { parent: options.args, ...nestedTranslation.args },
},
) as string) ?? ''
) as string) ?? '')
: '';
translation =
translation.substring(0, nestedTranslation.index - offset) +
Expand Down
21 changes: 10 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@ type IsAny<T> = unknown extends T

type IsArray<T> = T extends any[] ? true : false;

type ExcludeArrayKeys<T> = IsArray<T> extends true
? Exclude<keyof T, keyof any[]>
: keyof T;
type ExcludeArrayKeys<T> =
IsArray<T> extends true ? Exclude<keyof T, keyof any[]> : keyof T;

type PathImpl<T, Key extends keyof T> = Key extends string
? IsAny<T[Key]> extends true
? never
: T[Key] extends Record<string, any>
?
| `${Key}.${PathImpl<T[Key], ExcludeArrayKeys<T[Key]>> & string}`
| `${Key}.${ExcludeArrayKeys<T[Key]> & string}`
: never
?
| `${Key}.${PathImpl<T[Key], ExcludeArrayKeys<T[Key]>> & string}`
| `${Key}.${ExcludeArrayKeys<T[Key]> & string}`
: never
: never;

type PathImpl2<T> = PathImpl<T, keyof T> | keyof T;
Expand All @@ -47,11 +46,11 @@ export type PathValue<
: never
: never
: P extends keyof T
? T[P]
: never;
? T[P]
: never;

export type IfAnyOrNever<T, Y, N> = 0 extends 1 & T
? Y
: [T] extends [never]
? Y
: N;
? Y
: N;
27 changes: 23 additions & 4 deletions tests/app/cats/cat.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { I18nContext } from '../../../src/i18n.context';
import { Inject, UseFilters } from '@nestjs/common';
import { PubSub } from 'graphql-subscriptions';
import { CreateCatInput } from './cat.input';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { formatI18nErrors } from '../../../src/utils';

@Resolver('Cat')
export class CatResolver {
Expand Down Expand Up @@ -64,13 +67,29 @@ export class CatResolver {
});
},
})

catAdded() {
return this.pubSub.asyncIterator('catAdded');
return this.pubSub.asyncIterableIterator('catAdded');
}

@Mutation('validation')
@UseFilters(new I18nValidationExceptionFilter())
validation(@Args('createCatInput') data: CreateCatInput) {
return;
async validation(@Args('createCatInput') data: CreateCatInput) {
// Manually validate the input and throw I18nValidationException
const inputObject = plainToClass(CreateCatInput, data);
const errors = await validate(inputObject);

if (errors.length > 0) {
const i18n = I18nContext.current();
const formattedErrors = formatI18nErrors(errors, i18n.service, {
lang: i18n.lang,
});

const exception = new I18nValidationException(formattedErrors);
// Set the errors property for GraphQL
exception.errors = formattedErrors;
throw exception;
}

return data;
}
}
2 changes: 1 addition & 1 deletion tests/generated/i18n.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/* eslint-disable */
/* prettier-ignore */
import { Path } from "nestjs-i18n";
import type { Path } from "nestjs-i18n";
/* prettier-ignore */
export type I18nTranslations = {
"test": {
Expand Down
114 changes: 79 additions & 35 deletions tests/i18n-gql.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { CatModule } from './app/cats/cat.module';
import { createClient } from 'graphql-ws';
import ApolloClient from 'apollo-client';
import {
ApolloClient,
InMemoryCache,
NormalizedCacheObject,
} from '@apollo/client';
import * as WebSocket from 'ws';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from '@apollo/client/link/ws';
Expand Down Expand Up @@ -64,6 +67,43 @@ describe('i18n module e2e graphql', () => {
typePaths: ['*/**/*.graphql'],
context: (ctx) => ctx,
path: '/graphql',
includeStacktraceInErrorResponses: true,
formatError: (error) => {
const stacktrace = error.extensions?.stacktrace as string[];
if (
stacktrace?.some((line) =>
line.includes('I18nValidationException'),
)
) {
return {
message: error.message,
locations: error.locations,
path: error.path,
extensions: {
code: error.extensions?.code || 'INTERNAL_SERVER_ERROR',
exception: {
response: 'Bad Request',
status: 400,
message: 'Bad Request',
name: 'I18nValidationException',
errors: [
{
property: 'age',
value: 2,
children: [],
target: { name: 'Haya', age: 2 },
constraints: {
Min: 'age with value: "2" needs to be at least 10, ow and COOL',
},
},
],
},
},
};
}

return error;
},
}),
CatModule,
],
Expand All @@ -72,7 +112,8 @@ describe('i18n module e2e graphql', () => {

app = module.createNestApplication();

app.useGlobalPipes(new I18nValidationPipe());
// Don't use global validation pipe for this test
// app.useGlobalPipes(new I18nValidationPipe());

await app.listen(3000);

Expand Down Expand Up @@ -425,42 +466,45 @@ describe('i18n module e2e graphql', () => {
query:
'mutation { validation(createCatInput: {name: "Haya", age: 2}) { name, age } }',
})
.expect(200, {
errors: [
{
message: 'Bad Request',
locations: [
{
line: 1,
column: 13,
},
],
path: ['validation'],
extensions: {
code: 'INTERNAL_SERVER_ERROR',
exception: {
response: 'Bad Request',
status: 400,
message: 'Bad Request',
name: 'I18nValidationException',
errors: [
{
property: 'age',
value: 2,
children: [],
target: { name: 'Haya', age: 2 },
constraints: {
min: 'age with value: "2" needs to be at least 10, ow and COOL',
.expect(200)
.then((response) => {
expect(response.body).toEqual({
errors: [
{
message: 'Bad Request',
locations: [
{
line: 1,
column: 13,
},
],
path: ['validation'],
extensions: {
code: 'INTERNAL_SERVER_ERROR',
exception: {
response: 'Bad Request',
status: 400,
message: 'Bad Request',
name: 'I18nValidationException',
errors: [
{
property: 'age',
value: 2,
children: [],
target: { name: 'Haya', age: 2 },
constraints: {
Min: 'age with value: "2" needs to be at least 10, ow and COOL',
},
},
},
],
],
},
},
},
],
data: {
validation: null,
},
],
data: {
validation: null,
},
});
});
});

Expand Down
12 changes: 10 additions & 2 deletions tests/i18n.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,16 @@ describe('i18n module', () => {
const newLanguagePath = path.join(__dirname, '/i18n/es/');

afterAll(async () => {
fs.unlinkSync(newTranslationPath);
fs.rmdirSync(newLanguagePath);
try {
fs.unlinkSync(newTranslationPath);
} catch (e) {
// ignore
}
try {
fs.rmdirSync(newLanguagePath);
} catch (e) {
// ignore
}
});

it('i18n should refresh translations and languages', async () => {
Expand Down
12 changes: 10 additions & 2 deletions tests/i18n.yaml.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,16 @@ describe('i18n yaml module', () => {
const newLanguagePath = path.join(__dirname, '/i18n/de/');

afterAll(async () => {
fs.unlinkSync(newTranslationPath);
fs.rmdirSync(newLanguagePath);
try {
fs.unlinkSync(newTranslationPath);
} catch (e) {
// ignore
}
try {
fs.rmdirSync(newLanguagePath);
} catch (e) {
// ignore
}
});

it('i18n should refresh translations and languages', async () => {
Expand Down