From 01fcc62c2246e05ecfe2f7383d9fed62e4588512 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Tue, 31 Dec 2024 20:24:04 +0100 Subject: [PATCH 1/8] name to name_en and added name_ar --- api/db/migrations/0003_flaky_johnny_blaze.sql | 6 + api/db/migrations/meta/0003_snapshot.json | 470 ++++++++++++++++++ api/db/migrations/meta/_journal.json | 7 + api/src/contribution/types.ts | 6 +- api/src/contributor/controller.ts | 15 +- api/src/contributor/repository.ts | 17 +- api/src/contributor/table.ts | 4 +- api/src/contributor/types.ts | 10 +- api/src/digest/cron.ts | 15 +- api/src/project/controller.ts | 6 +- api/src/project/types.ts | 4 +- packages/models/src/contributor/index.ts | 7 +- packages/utils/src/language/index.ts | 3 + packages/utils/src/ts/index.ts | 17 + 14 files changed, 561 insertions(+), 26 deletions(-) create mode 100644 api/db/migrations/0003_flaky_johnny_blaze.sql create mode 100644 api/db/migrations/meta/0003_snapshot.json create mode 100644 packages/utils/src/language/index.ts diff --git a/api/db/migrations/0003_flaky_johnny_blaze.sql b/api/db/migrations/0003_flaky_johnny_blaze.sql new file mode 100644 index 000000000..03e5b663d --- /dev/null +++ b/api/db/migrations/0003_flaky_johnny_blaze.sql @@ -0,0 +1,6 @@ +ALTER TABLE "contributors" RENAME COLUMN "name" TO "name_en";--> statement-breakpoint +ALTER TABLE "contributor_repository_relation" ALTER COLUMN "run_id" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "project_tag_relation" ALTER COLUMN "run_id" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "repositories" ALTER COLUMN "run_id" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "tags" ALTER COLUMN "run_id" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "contributors" ADD COLUMN "name_ar" text DEFAULT '' NOT NULL; \ No newline at end of file diff --git a/api/db/migrations/meta/0003_snapshot.json b/api/db/migrations/meta/0003_snapshot.json new file mode 100644 index 000000000..0f9acd39d --- /dev/null +++ b/api/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,470 @@ +{ + "id": "be25da33-1037-4e37-8e46-a3ac74a225ad", + "prevId": "429ed010-076d-4544-a9c0-7c9d7d8073ea", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.contributions": { + "name": "contributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_count": { + "name": "activity_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributions_repository_id_repositories_id_fk": { + "name": "contributions_repository_id_repositories_id_fk", + "tableFrom": "contributions", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributions_contributor_id_contributors_id_fk": { + "name": "contributions_contributor_id_contributors_id_fk", + "tableFrom": "contributions", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributions_url_unique": { + "name": "contributions_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.contributor_repository_relation": { + "name": "contributor_repository_relation", + "schema": "", + "columns": { + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributor_repository_relation_contributor_id_contributors_id_fk": { + "name": "contributor_repository_relation_contributor_id_contributors_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributor_repository_relation_repository_id_repositories_id_fk": { + "name": "contributor_repository_relation_repository_id_repositories_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contributor_repository_relation_pk": { + "name": "contributor_repository_relation_pk", + "columns": [ + "contributor_id", + "repository_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.contributors": { + "name": "contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_ar": { + "name": "name_ar", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "name_en": { + "name": "name_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributors_url_unique": { + "name": "contributors_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.project_tag_relation": { + "name": "project_tag_relation", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_tag_relation_project_id_projects_id_fk": { + "name": "project_tag_relation_project_id_projects_id_fk", + "tableFrom": "project_tag_relation", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_tag_relation_pk": { + "name": "project_tag_relation_pk", + "columns": [ + "project_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "repositories_project_id_projects_id_fk": { + "name": "repositories_project_id_projects_id_fk", + "tableFrom": "repositories", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_provider_owner_name_unique": { + "name": "repositories_provider_owner_name_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "owner", + "name" + ] + } + } + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/db/migrations/meta/_journal.json b/api/db/migrations/meta/_journal.json index 0369048ea..95692df37 100644 --- a/api/db/migrations/meta/_journal.json +++ b/api/db/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1735482799289, "tag": "0002_cooing_thena", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1735672300337, + "tag": "0003_flaky_johnny_blaze", + "breakpoints": true } ] } \ No newline at end of file diff --git a/api/src/contribution/types.ts b/api/src/contribution/types.ts index 093b6b968..e020b7aea 100644 --- a/api/src/contribution/types.ts +++ b/api/src/contribution/types.ts @@ -1,5 +1,5 @@ import { ContributionEntity } from "@dzcode.io/models/dist/contribution"; -import { ContributorEntity } from "@dzcode.io/models/dist/contributor"; +import { ContributorNoLang } from "@dzcode.io/models/dist/contributor"; import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; import { GeneralResponse } from "src/app/types"; @@ -10,7 +10,7 @@ export interface GetContributionsResponse extends GeneralResponse { repository: Pick & { project: Pick; }; - contributor: Pick; + contributor: Pick; } >; } @@ -23,7 +23,7 @@ export interface GetContributionResponse extends GeneralResponse { repository: Pick & { project: Pick; }; - contributor: Pick; + contributor: Pick; }; } diff --git a/api/src/contributor/controller.ts b/api/src/contributor/controller.ts index fccc872b3..14d9b7bb3 100644 --- a/api/src/contributor/controller.ts +++ b/api/src/contributor/controller.ts @@ -10,6 +10,7 @@ import { } from "./types"; import { ProjectRepository } from "src/project/repository"; import { ContributionRepository } from "src/contribution/repository"; +import { Language } from "@dzcode.io/utils/dist/language"; @Service() @Controller("/contributors") @@ -22,7 +23,9 @@ export class ContributorController { @Get("/") public async getContributors(): Promise { - const contributors = await this.contributorRepository.findForList(); + // todo: lang query param + const lang: Language = "en"; + const contributors = await this.contributorRepository.findForList(lang); return { contributors, @@ -40,8 +43,11 @@ export class ContributorController { @Get("/:id") public async getContributor(@Param("id") id: string): Promise { + // todo: lang query param + const lang: Language = "en"; + const [contributor, projects, contributions] = await Promise.all([ - this.contributorRepository.findWithStats(id), + this.contributorRepository.findWithStats(id, lang), this.projectRepository.findForContributor(id), this.contributionRepository.findForContributor(id), ]); @@ -59,7 +65,10 @@ export class ContributorController { @Get("/:id/name") public async getContributorName(@Param("id") id: string): Promise { - const contributor = await this.contributorRepository.findName(id); + // todo: lang query param + const lang: Language = "en"; + + const contributor = await this.contributorRepository.findName(id, lang); if (!contributor) throw new NotFoundError("Contributor not found"); diff --git a/api/src/contributor/repository.ts b/api/src/contributor/repository.ts index 619f2694b..ad744a454 100644 --- a/api/src/contributor/repository.ts +++ b/api/src/contributor/repository.ts @@ -11,16 +11,17 @@ import { ContributorRow, contributorsTable, } from "./table"; +import { Language } from "@dzcode.io/utils/dist/language"; @Service() export class ContributorRepository { constructor(private readonly postgresService: PostgresService) {} - public async findName(contributorId: string) { + public async findName(contributorId: string, lang: Language) { const statement = sql` SELECT ${contributorsTable.id}, - ${contributorsTable.name} + ${contributorsTable[`name_${lang}`]} FROM ${contributorsTable} WHERE @@ -38,11 +39,11 @@ export class ContributorRepository { return camelCased; } - public async findForProject(projectId: string) { + public async findForProject(projectId: string, lang: Language) { const statement = sql` SELECT ${contributorsTable.id}, - ${contributorsTable.name}, + ${contributorsTable[`name_${lang}`]}, ${contributorsTable.avatarUrl}, sum(${contributorRepositoryRelationTable.score}) as ranking FROM @@ -66,11 +67,11 @@ export class ContributorRepository { return camelCased; } - public async findForList() { + public async findForList(lang: Language) { const statement = sql` SELECT ${contributorsTable.id}, - ${contributorsTable.name}, + ${contributorsTable[`name_${lang}`]}, ${contributorsTable.avatarUrl}, sum(${contributorRepositoryRelationTable.score}) as total_contribution_score, count(DISTINCT ${contributorRepositoryRelationTable.repositoryId}) as total_repository_count, @@ -151,11 +152,11 @@ export class ContributorRepository { .where(ne(contributorsTable.runId, runId)); } - public async findWithStats(contributorId: string) { + public async findWithStats(contributorId: string, lang: Language) { const statement = sql` SELECT ${contributorsTable.id}, - ${contributorsTable.name}, + ${contributorsTable[`name_${lang}`]}, ${contributorsTable.avatarUrl}, ${contributorsTable.username}, ${contributorsTable.url}, diff --git a/api/src/contributor/table.ts b/api/src/contributor/table.ts index e602deb95..3eb5d4264 100644 --- a/api/src/contributor/table.ts +++ b/api/src/contributor/table.ts @@ -9,7 +9,9 @@ export const contributorsTable = pgTable("contributors", { .notNull() .default(sql`CURRENT_TIMESTAMP`), runId: text("run_id").notNull(), - name: text("name").notNull(), + // todo: remove default value after migration + name_ar: text("name_ar").notNull().default(""), + name_en: text("name_en").notNull(), username: text("username").notNull(), url: text("url").notNull().unique(), avatarUrl: text("avatar_url").notNull(), diff --git a/api/src/contributor/types.ts b/api/src/contributor/types.ts index 09aac0425..4d510f2cf 100644 --- a/api/src/contributor/types.ts +++ b/api/src/contributor/types.ts @@ -1,15 +1,15 @@ import { ContributionEntity } from "@dzcode.io/models/dist/contribution"; -import { ContributorEntity } from "@dzcode.io/models/dist/contributor"; +import { ContributorNoLang } from "@dzcode.io/models/dist/contributor"; import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { GeneralResponse } from "src/app/types"; export interface GetContributorsForSitemapResponse extends GeneralResponse { - contributors: Array>; + contributors: Array>; } export interface GetContributorsResponse extends GeneralResponse { contributors: Array< - Pick & { + Pick & { ranking: number; totalContributionScore: number; totalRepositoryCount: number; @@ -18,7 +18,7 @@ export interface GetContributorsResponse extends GeneralResponse { } export interface GetContributorResponse extends GeneralResponse { - contributor: Omit & { + contributor: Omit & { ranking: number; totalContributionScore: number; totalRepositoryCount: number; @@ -35,5 +35,5 @@ export interface GetContributorResponse extends GeneralResponse { } export interface GetContributorNameResponse extends GeneralResponse { - contributor: Pick; + contributor: Pick; } diff --git a/api/src/digest/cron.ts b/api/src/digest/cron.ts index 63f764831..17e89fd84 100644 --- a/api/src/digest/cron.ts +++ b/api/src/digest/cron.ts @@ -128,8 +128,13 @@ export class DigestCron { if (githubUser.type !== "User") continue; + // todo: call AIService + const name_en = githubUser.name || githubUser.login; + const name_ar = `ar ${name_en}`; + const contributorEntity: ContributorRow = { - name: githubUser.name || githubUser.login, + name_en, + name_ar, username: githubUser.login, url: githubUser.html_url, avatarUrl: githubUser.avatar_url, @@ -177,8 +182,14 @@ export class DigestCron { const contributor = await this.githubService.getUser({ username: repoContributor.login, }); + + // todo: call AIService + const name_en = contributor.name || contributor.login; + const name_ar = `ar ${name_en}`; + const contributorEntity: ContributorRow = { - name: contributor.name || contributor.login, + name_en, + name_ar, username: contributor.login, url: contributor.html_url, avatarUrl: contributor.avatar_url, diff --git a/api/src/project/controller.ts b/api/src/project/controller.ts index 916801a7f..ce4c998de 100644 --- a/api/src/project/controller.ts +++ b/api/src/project/controller.ts @@ -11,6 +11,7 @@ import { import { RepositoryRepository } from "src/repository/repository"; import { ContributorRepository } from "src/contributor/repository"; import { ContributionRepository } from "src/contribution/repository"; +import { Language } from "@dzcode.io/utils/dist/language"; @Service() @Controller("/projects") @@ -42,10 +43,13 @@ export class ProjectController { @Get("/:id") public async getProject(@Param("id") id: string): Promise { + // todo: lang query param + const lang: Language = "en"; + const [project, repositories, contributors, contributions] = await Promise.all([ this.projectRepository.findWithStats(id), this.repositoryRepository.findForProject(id), - this.contributorRepository.findForProject(id), + this.contributorRepository.findForProject(id, lang), this.contributionRepository.findForProject(id), ]); diff --git a/api/src/project/types.ts b/api/src/project/types.ts index 3e972912d..7eb631f1a 100644 --- a/api/src/project/types.ts +++ b/api/src/project/types.ts @@ -1,5 +1,5 @@ import { ContributionEntity } from "@dzcode.io/models/dist/contribution"; -import { ContributorEntity } from "@dzcode.io/models/dist/contributor"; +import { ContributorNoLang } from "@dzcode.io/models/dist/contributor"; import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; import { TagEntity } from "@dzcode.io/models/dist/tag"; @@ -31,7 +31,7 @@ export interface GetProjectResponse extends GeneralResponse { } >; contributors: Array< - Omit & { + Omit & { score: number; } >; diff --git a/packages/models/src/contributor/index.ts b/packages/models/src/contributor/index.ts index e3bf8596c..eff59ac09 100644 --- a/packages/models/src/contributor/index.ts +++ b/packages/models/src/contributor/index.ts @@ -1,8 +1,13 @@ import { BaseEntity } from "src/_base"; +import { StripLanguage } from "@dzcode.io/utils/dist/ts"; +import { Language } from "@dzcode.io/utils/dist/language"; export type ContributorEntity = BaseEntity & { - name: string; + name_ar: string; + name_en: string; username: string; url: string; avatarUrl: string; }; + +export type ContributorNoLang = StripLanguage; diff --git a/packages/utils/src/language/index.ts b/packages/utils/src/language/index.ts new file mode 100644 index 000000000..de348c48f --- /dev/null +++ b/packages/utils/src/language/index.ts @@ -0,0 +1,3 @@ +export const LANGUAGES = ["ar", "en"] as const; + +export type Language = (typeof LANGUAGES)[number]; diff --git a/packages/utils/src/ts/index.ts b/packages/utils/src/ts/index.ts index 2d971000b..989131859 100644 --- a/packages/utils/src/ts/index.ts +++ b/packages/utils/src/ts/index.ts @@ -1,3 +1,5 @@ +import { Language } from "../language"; + export type Flatten = T extends any[] ? T[number] : T; // eslint-disable-line @typescript-eslint/no-explicit-any export type OptionalPropertiesOf = Required< @@ -44,3 +46,18 @@ export type PyramidSplitString< export type PartialWithOneRequiredKey }> = Partial & U[keyof U]; + +/** + * find the key that ends with the language code and remove language code from it + * @example + * type Post = { + * title_ar: string; + * title_en: string; + * author: string; + * } + * type PostNoLang = StripLanguage<"ar" | "en", Post>; + * // { title: string; author: string; } + */ +export type StripLanguage> = { + [K in keyof M as K extends `${infer F}_${T}` ? F : K]: M[K]; +}; From 5841d1f01b00b8544a0ade5545350f502ee46b42 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Tue, 31 Dec 2024 23:09:46 +0100 Subject: [PATCH 2/8] updated endpoints to use `lang` param --- api/src/_utils/language.ts | 7 +++++ api/src/contribution/controller.ts | 16 +++++++---- api/src/contribution/repository.ts | 9 +++--- api/src/contributor/controller.ts | 26 ++++++++--------- api/src/contributor/repository.ts | 18 ++++++------ api/src/project/controller.ts | 12 ++++---- packages/models/src/contributor/index.ts | 3 +- packages/models/src/language/index.ts | 22 +++++++++------ packages/utils/src/fetch/factory.ts | 6 ++-- packages/utils/src/language/index.ts | 5 ++-- packages/utils/src/ts/index.ts | 6 ++-- .../functions/w/contributions-sitemap.xml.ts | 26 ++++++++--------- .../functions/w/contributors-sitemap.xml.ts | 28 +++++++++---------- .../functions/w/projects-sitemap.xml.ts | 26 ++++++++--------- web/cloudflare/handler/contributor.ts | 7 ++--- web/lighthouserc.cjs | 4 +-- web/src/_build/pages/index.ts | 5 ++-- web/src/_build/pages/static-pages.ts | 4 +-- web/src/_build/sitemap.ts | 4 +-- web/src/_entry/app.tsx | 4 +-- web/src/components/link.tsx | 5 ++-- web/src/components/locale/languages.ts | 6 ---- web/src/components/redirect.tsx | 5 ++-- web/src/components/top-bar.tsx | 3 +- web/src/redux/actions/settings.ts | 5 ++-- web/src/redux/slices/settings.ts | 4 +-- web/src/utils/fetch.ts | 3 +- web/src/utils/website-language.ts | 7 +++-- 28 files changed, 142 insertions(+), 134 deletions(-) create mode 100644 api/src/_utils/language.ts delete mode 100644 web/src/components/locale/languages.ts diff --git a/api/src/_utils/language.ts b/api/src/_utils/language.ts new file mode 100644 index 000000000..8b290b511 --- /dev/null +++ b/api/src/_utils/language.ts @@ -0,0 +1,7 @@ +import { LANGUAGE_CODES, LanguageCode } from "@dzcode.io/utils/dist/language"; +import { IsIn } from "class-validator"; + +export class LanguageQuery { + @IsIn(LANGUAGE_CODES) + lang!: LanguageCode; +} diff --git a/api/src/contribution/controller.ts b/api/src/contribution/controller.ts index b42b20efb..b74437022 100644 --- a/api/src/contribution/controller.ts +++ b/api/src/contribution/controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, NotFoundError, Param } from "routing-controllers"; +import { Controller, Get, NotFoundError, Param, QueryParams } from "routing-controllers"; import { Service } from "typedi"; import { ContributionRepository } from "./repository"; @@ -8,6 +8,7 @@ import { GetContributionsResponse, GetContributionsForSitemapResponse, } from "./types"; +import { LanguageQuery } from "src/_utils/language"; @Service() @Controller("/contributions") @@ -15,8 +16,10 @@ export class ContributionController { constructor(private readonly contributionRepository: ContributionRepository) {} @Get("/") - public async getContributions(): Promise { - const contributions = await this.contributionRepository.findForList(); + public async getContributions( + @QueryParams() { lang }: LanguageQuery, + ): Promise { + const contributions = await this.contributionRepository.findForList(lang); return { contributions, @@ -33,8 +36,11 @@ export class ContributionController { } @Get("/:id") - public async getContribution(@Param("id") id: string): Promise { - const contribution = await this.contributionRepository.findByIdWithStats(id); + public async getContribution( + @Param("id") id: string, + @QueryParams() { lang }: LanguageQuery, + ): Promise { + const contribution = await this.contributionRepository.findByIdWithStats(id, lang); return { contribution, diff --git a/api/src/contribution/repository.ts b/api/src/contribution/repository.ts index 598b49315..36db5d477 100644 --- a/api/src/contribution/repository.ts +++ b/api/src/contribution/repository.ts @@ -9,6 +9,7 @@ import { PostgresService } from "src/postgres/service"; import { Service } from "typedi"; import { ContributionRow, contributionsTable } from "./table"; +import { LanguageCode } from "@dzcode.io/utils/dist/language"; @Service() export class ContributionRepository { @@ -113,7 +114,7 @@ export class ContributionRepository { .where(ne(contributionsTable.runId, runId)); } - public async findForList() { + public async findForList(lang: LanguageCode) { const statement = sql` SELECT p.id as id, @@ -146,7 +147,7 @@ export class ContributionRepository { 'id', cr.id, 'name', - cr.name, + cr.name_${sql.raw(lang)}, 'username', cr.username, 'avatar_url', @@ -187,7 +188,7 @@ export class ContributionRepository { return sortedUpdatedAt; } - public async findByIdWithStats(id: string) { + public async findByIdWithStats(id: string, lang: LanguageCode) { const statement = sql` SELECT p.id as id, @@ -220,7 +221,7 @@ export class ContributionRepository { 'id', cr.id, 'name', - cr.name, + cr.name_${sql.raw(lang)}, 'username', cr.username, 'avatar_url', diff --git a/api/src/contributor/controller.ts b/api/src/contributor/controller.ts index 14d9b7bb3..0b9811106 100644 --- a/api/src/contributor/controller.ts +++ b/api/src/contributor/controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, NotFoundError, Param } from "routing-controllers"; +import { Controller, Get, NotFoundError, Param, QueryParams } from "routing-controllers"; import { Service } from "typedi"; import { ContributorRepository } from "./repository"; @@ -10,7 +10,7 @@ import { } from "./types"; import { ProjectRepository } from "src/project/repository"; import { ContributionRepository } from "src/contribution/repository"; -import { Language } from "@dzcode.io/utils/dist/language"; +import { LanguageQuery } from "src/_utils/language"; @Service() @Controller("/contributors") @@ -22,9 +22,9 @@ export class ContributorController { ) {} @Get("/") - public async getContributors(): Promise { - // todo: lang query param - const lang: Language = "en"; + public async getContributors( + @QueryParams() { lang }: LanguageQuery, + ): Promise { const contributors = await this.contributorRepository.findForList(lang); return { @@ -42,10 +42,10 @@ export class ContributorController { } @Get("/:id") - public async getContributor(@Param("id") id: string): Promise { - // todo: lang query param - const lang: Language = "en"; - + public async getContributor( + @Param("id") id: string, + @QueryParams() { lang }: LanguageQuery, + ): Promise { const [contributor, projects, contributions] = await Promise.all([ this.contributorRepository.findWithStats(id, lang), this.projectRepository.findForContributor(id), @@ -64,10 +64,10 @@ export class ContributorController { } @Get("/:id/name") - public async getContributorName(@Param("id") id: string): Promise { - // todo: lang query param - const lang: Language = "en"; - + public async getContributorName( + @Param("id") id: string, + @QueryParams() { lang }: LanguageQuery, + ): Promise { const contributor = await this.contributorRepository.findName(id, lang); if (!contributor) throw new NotFoundError("Contributor not found"); diff --git a/api/src/contributor/repository.ts b/api/src/contributor/repository.ts index ad744a454..74e911aa9 100644 --- a/api/src/contributor/repository.ts +++ b/api/src/contributor/repository.ts @@ -11,17 +11,17 @@ import { ContributorRow, contributorsTable, } from "./table"; -import { Language } from "@dzcode.io/utils/dist/language"; +import { LanguageCode } from "@dzcode.io/utils/dist/language"; @Service() export class ContributorRepository { constructor(private readonly postgresService: PostgresService) {} - public async findName(contributorId: string, lang: Language) { + public async findName(contributorId: string, lang: LanguageCode) { const statement = sql` SELECT ${contributorsTable.id}, - ${contributorsTable[`name_${lang}`]} + ${contributorsTable[`name_${lang}`]} as name FROM ${contributorsTable} WHERE @@ -39,11 +39,11 @@ export class ContributorRepository { return camelCased; } - public async findForProject(projectId: string, lang: Language) { + public async findForProject(projectId: string, lang: LanguageCode) { const statement = sql` SELECT ${contributorsTable.id}, - ${contributorsTable[`name_${lang}`]}, + ${contributorsTable[`name_${lang}`]} as name, ${contributorsTable.avatarUrl}, sum(${contributorRepositoryRelationTable.score}) as ranking FROM @@ -67,11 +67,11 @@ export class ContributorRepository { return camelCased; } - public async findForList(lang: Language) { + public async findForList(lang: LanguageCode) { const statement = sql` SELECT ${contributorsTable.id}, - ${contributorsTable[`name_${lang}`]}, + ${contributorsTable[`name_${lang}`]} as name, ${contributorsTable.avatarUrl}, sum(${contributorRepositoryRelationTable.score}) as total_contribution_score, count(DISTINCT ${contributorRepositoryRelationTable.repositoryId}) as total_repository_count, @@ -152,11 +152,11 @@ export class ContributorRepository { .where(ne(contributorsTable.runId, runId)); } - public async findWithStats(contributorId: string, lang: Language) { + public async findWithStats(contributorId: string, lang: LanguageCode) { const statement = sql` SELECT ${contributorsTable.id}, - ${contributorsTable[`name_${lang}`]}, + ${contributorsTable[`name_${lang}`]} as name, ${contributorsTable.avatarUrl}, ${contributorsTable.username}, ${contributorsTable.url}, diff --git a/api/src/project/controller.ts b/api/src/project/controller.ts index ce4c998de..afa514d42 100644 --- a/api/src/project/controller.ts +++ b/api/src/project/controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, NotFoundError, Param } from "routing-controllers"; +import { Controller, Get, NotFoundError, Param, QueryParams } from "routing-controllers"; import { Service } from "typedi"; import { ProjectRepository } from "./repository"; @@ -11,7 +11,7 @@ import { import { RepositoryRepository } from "src/repository/repository"; import { ContributorRepository } from "src/contributor/repository"; import { ContributionRepository } from "src/contribution/repository"; -import { Language } from "@dzcode.io/utils/dist/language"; +import { LanguageQuery } from "src/_utils/language"; @Service() @Controller("/projects") @@ -42,10 +42,10 @@ export class ProjectController { } @Get("/:id") - public async getProject(@Param("id") id: string): Promise { - // todo: lang query param - const lang: Language = "en"; - + public async getProject( + @Param("id") id: string, + @QueryParams() { lang }: LanguageQuery, + ): Promise { const [project, repositories, contributors, contributions] = await Promise.all([ this.projectRepository.findWithStats(id), this.repositoryRepository.findForProject(id), diff --git a/packages/models/src/contributor/index.ts b/packages/models/src/contributor/index.ts index eff59ac09..834188522 100644 --- a/packages/models/src/contributor/index.ts +++ b/packages/models/src/contributor/index.ts @@ -1,6 +1,5 @@ import { BaseEntity } from "src/_base"; import { StripLanguage } from "@dzcode.io/utils/dist/ts"; -import { Language } from "@dzcode.io/utils/dist/language"; export type ContributorEntity = BaseEntity & { name_ar: string; @@ -10,4 +9,4 @@ export type ContributorEntity = BaseEntity & { avatarUrl: string; }; -export type ContributorNoLang = StripLanguage; +export type ContributorNoLang = StripLanguage; diff --git a/packages/models/src/language/index.ts b/packages/models/src/language/index.ts index 146f11546..0ffa4388b 100644 --- a/packages/models/src/language/index.ts +++ b/packages/models/src/language/index.ts @@ -1,11 +1,17 @@ -export const allLanguages = [ +import { LanguageCode } from "@dzcode.io/utils/dist/language"; + +export interface Language { + code: LanguageCode; + shortLabel: string; + label: string; + direction: "ltr" | "rtl"; + baseUrl: string; +} + +// todo: renamed to LANGUAGES +export const Languages: Language[] = [ { code: "en", shortLabel: "EN", label: "English", direction: "ltr", baseUrl: "" }, { code: "ar", shortLabel: "ع", label: "العربية", direction: "rtl", baseUrl: "/ar" }, -] as const; +]; -export interface LanguageEntity { - code: (typeof allLanguages)[number]["code"]; - shortLabel: (typeof allLanguages)[number]["shortLabel"]; - label: (typeof allLanguages)[number]["label"]; - direction: (typeof allLanguages)[number]["direction"]; -} +export const DefaultLanguage: Language = Languages[0]; diff --git a/packages/utils/src/fetch/factory.ts b/packages/utils/src/fetch/factory.ts index b4b237db7..c50cefd7a 100644 --- a/packages/utils/src/fetch/factory.ts +++ b/packages/utils/src/fetch/factory.ts @@ -1,4 +1,5 @@ import { FullstackConfig } from "../config"; +import { LanguageCode } from "../language"; interface Endpoint { params?: Record; @@ -7,14 +8,15 @@ interface Endpoint { } export const fetchV2Factory = - (fullstackConfig: FullstackConfig) => + (fullstackConfig: FullstackConfig, lang: LanguageCode) => async ( endpoint: E, config: Pick>, ): Promise => { const { body, params, query } = config as Endpoint; - const queryString = query ? "?" + query.map(([key, value]) => `${key}=${value}`).join("&") : ""; + const queryWithLang = [...(query || []), ["lang", lang]]; + const queryString = "?" + queryWithLang.map(([key, value]) => `${key}=${value}`).join("&"); const domain = (endpoint as string).slice(0, (endpoint as string).indexOf(":")); let url = (endpoint as string).slice(domain.length + 1); diff --git a/packages/utils/src/language/index.ts b/packages/utils/src/language/index.ts index de348c48f..8dea1080c 100644 --- a/packages/utils/src/language/index.ts +++ b/packages/utils/src/language/index.ts @@ -1,3 +1,2 @@ -export const LANGUAGES = ["ar", "en"] as const; - -export type Language = (typeof LANGUAGES)[number]; +export const LANGUAGE_CODES = ["ar", "en"] as const; +export type LanguageCode = (typeof LANGUAGE_CODES)[number]; diff --git a/packages/utils/src/ts/index.ts b/packages/utils/src/ts/index.ts index 989131859..b1a4501d7 100644 --- a/packages/utils/src/ts/index.ts +++ b/packages/utils/src/ts/index.ts @@ -1,4 +1,4 @@ -import { Language } from "../language"; +import { LanguageCode } from "../language"; export type Flatten = T extends any[] ? T[number] : T; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -58,6 +58,6 @@ export type PartialWithOneRequiredKey }> = P * type PostNoLang = StripLanguage<"ar" | "en", Post>; * // { title: string; author: string; } */ -export type StripLanguage> = { - [K in keyof M as K extends `${infer F}_${T}` ? F : K]: M[K]; +export type StripLanguage> = { + [K in keyof M as K extends `${infer F}_${LanguageCode}` ? F : K]: M[K]; }; diff --git a/web/cloudflare/functions/w/contributions-sitemap.xml.ts b/web/cloudflare/functions/w/contributions-sitemap.xml.ts index 0c84078b4..d032dfaed 100644 --- a/web/cloudflare/functions/w/contributions-sitemap.xml.ts +++ b/web/cloudflare/functions/w/contributions-sitemap.xml.ts @@ -1,6 +1,6 @@ import { Env } from "handler/contribution"; import { environments } from "@dzcode.io/utils/dist/config/environment"; -import { allLanguages, LanguageEntity } from "@dzcode.io/models/dist/language"; +import { Language, Languages } from "@dzcode.io/models/dist/language"; import { getContributionURL } from "@dzcode.io/web/dist/utils/contribution"; import { fsConfig } from "@dzcode.io/utils/dist/config"; import { fetchV2Factory } from "@dzcode.io/utils/dist/fetch/factory"; @@ -20,21 +20,21 @@ export const onRequest: PagesFunction = async (context) => { stage = "development"; } const fullstackConfig = fsConfig(stage); - const fetchV2 = fetchV2Factory(fullstackConfig); + const links: Array<{ url: string; lang: Language["code"] }> = []; - const { contributions } = await fetchV2("api:contributions/for-sitemap", {}); + for (const lang of Languages) { + const fetchV2 = fetchV2Factory(fullstackConfig, lang.code); + const { contributions } = await fetchV2("api:contributions/for-sitemap", {}); - const hostname = "https://www.dzCode.io"; - const links = contributions.reduce<{ url: string; lang: LanguageEntity["code"] }[]>((pV, cV) => { - return [ - ...pV, - ...allLanguages.map(({ baseUrl, code }) => ({ - url: xmlEscape(`${baseUrl}${getContributionURL(cV)}`), - lang: code, - })), - ]; - }, []); + for (const contribution of contributions) { + links.push({ + url: xmlEscape(`${lang.baseUrl}${getContributionURL(contribution)}`), + lang: lang.code, + }); + } + } + const hostname = "https://www.dzCode.io"; const xml = ` = async (context) => { stage = "development"; } const fullstackConfig = fsConfig(stage); - const fetchV2 = fetchV2Factory(fullstackConfig); - - const { contributors } = await fetchV2("api:contributors/for-sitemap", {}); - - const hostname = "https://www.dzCode.io"; - const links = contributors.reduce<{ url: string; lang: LanguageEntity["code"] }[]>((pV, cV) => { - return [ - ...pV, - ...allLanguages.map(({ baseUrl, code }) => ({ - url: `${baseUrl}${getContributorURL(cV)}`, - lang: code, - })), - ]; - }, []); + const links: Array<{ url: string; lang: Language["code"] }> = []; + for (const lang of Languages) { + const fetchV2 = fetchV2Factory(fullstackConfig, lang.code); + const { contributors } = await fetchV2("api:contributors/for-sitemap", {}); + for (const contributor of contributors) { + links.push({ + url: `${lang.baseUrl}${getContributorURL(contributor)}`, + lang: lang.code, + }); + } + } + const hostname = "https://www.dzcode.io"; const xml = ` = async (context) => { stage = "development"; } const fullstackConfig = fsConfig(stage); - const fetchV2 = fetchV2Factory(fullstackConfig); - - const { projects } = await fetchV2("api:projects/for-sitemap", {}); + const links: Array<{ url: string; lang: Language["code"] }> = []; + for (const lang of Languages) { + const fetchV2 = fetchV2Factory(fullstackConfig, lang.code); + const { projects } = await fetchV2("api:projects/for-sitemap", {}); + for (const project of projects) { + links.push({ + url: `${lang.baseUrl}${getProjectURL(project)}`, + lang: lang.code, + }); + } + } const hostname = "https://www.dzCode.io"; - const links = projects.reduce<{ url: string; lang: LanguageEntity["code"] }[]>((pV, cV) => { - return [ - ...pV, - ...allLanguages.map(({ baseUrl, code }) => ({ - url: `${baseUrl}${getProjectURL(cV)}`, - lang: code, - })), - ]; - }, []); - const xml = ` = async (context) => { const pathName = new URL(context.request.url).pathname; const languageRegex = /^\/(ar|en)\//i; - const language = (pathName?.match(languageRegex)?.[1]?.toLowerCase() || - "en") as LanguageEntity["code"]; + const language = (pathName?.match(languageRegex)?.[1]?.toLowerCase() || "en") as LanguageCode; const notFound = language === "ar" ? notFoundAr : notFoundEn; const contributorIdRegex = /team\/(.*)/; @@ -44,7 +43,7 @@ export const handleContributorRequest: PagesFunction = async (context) => { plainLocalize(dictionary, language, key, "NO-TRANSLATION"); const fullstackConfig = fsConfig(stage); - const fetchV2 = fetchV2Factory(fullstackConfig); + const fetchV2 = fetchV2Factory(fullstackConfig, language); try { const { contributor } = await fetchV2("api:contributors/:id/name", { diff --git a/web/lighthouserc.cjs b/web/lighthouserc.cjs index 0a9bbf992..c86d8ee99 100644 --- a/web/lighthouserc.cjs +++ b/web/lighthouserc.cjs @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-require-imports -const { allLanguages } = require("@dzcode.io/models/dist/language"); +const { Languages } = require("@dzcode.io/models/dist/language"); const baseUrl = process.env.LH_TEST_BASE_URL; const serverBaseUrl = process.env.LH_SERVER_BASE_URL; @@ -20,7 +20,7 @@ module.exports = { collect: { url: urls.reduce((acc, path) => { return acc.concat( - allLanguages.map(({ code }) => `${baseUrl}${code === "en" ? "" : `/${code}`}${path}`), + Languages.map(({ code }) => `${baseUrl}${code === "en" ? "" : `/${code}`}${path}`), ); }, []), }, diff --git a/web/src/_build/pages/index.ts b/web/src/_build/pages/index.ts index 8e7e052e9..55d292df4 100644 --- a/web/src/_build/pages/index.ts +++ b/web/src/_build/pages/index.ts @@ -1,5 +1,4 @@ -import { LanguageEntity } from "@dzcode.io/models/dist/language"; - +import { LanguageCode } from "@dzcode.io/utils/dist/language"; import { AllDictionaryKeys } from "../../components/locale/dictionary"; import { staticPages } from "./static-pages"; import { templatePages } from "./template-pages"; @@ -11,7 +10,7 @@ export interface PageInfo { description: string; ogImage: string; keywords: string; - lang: LanguageEntity["code"]; + lang: LanguageCode; } export type PageInfoWithLocalKeys = Omit & { diff --git a/web/src/_build/pages/static-pages.ts b/web/src/_build/pages/static-pages.ts index a28e6eb97..4f18b5ecf 100644 --- a/web/src/_build/pages/static-pages.ts +++ b/web/src/_build/pages/static-pages.ts @@ -1,8 +1,8 @@ -import { allLanguages } from "@dzcode.io/models/dist/language"; import { plainLocalize } from "../../components/locale/utils"; import { dictionary, AllDictionaryKeys } from "../../components/locale/dictionary"; import { PageInfo, PageInfoWithLocalKeys } from "."; +import { Languages } from "@dzcode.io/models/dist/language"; const localize = (key: AllDictionaryKeys, language: string) => plainLocalize(dictionary, language, key, "NO-TRANSLATION"); @@ -61,7 +61,7 @@ const staticURLs: PageInfoWithLocalKeys[] = [ export const staticPages: PageInfo[] = staticURLs.reduce( (acc, { title, description, uri, ...page }) => [ ...acc, - ...allLanguages.map(({ code }) => ({ + ...Languages.map(({ code }) => ({ ...page, title: localize(title, code), description: localize(description, code), diff --git a/web/src/_build/sitemap.ts b/web/src/_build/sitemap.ts index ef95d394c..67ef0aa09 100644 --- a/web/src/_build/sitemap.ts +++ b/web/src/_build/sitemap.ts @@ -1,8 +1,8 @@ -import { allLanguages } from "@dzcode.io/models/dist/language"; import { createWriteStream } from "fs"; import { join } from "path"; import { SitemapStream } from "sitemap"; import { staticPages } from "./pages"; +import { Languages } from "@dzcode.io/models/dist/language"; const distFolder = "./bundle"; @@ -23,7 +23,7 @@ writeStream.on("error", (err) => { writeStream.on("ready", () => { sitemap.pipe(writeStream); urls.forEach((url) => { - const lang = allLanguages.find(({ code }) => `/${code}/` === url.substring(0, 4))?.code || "en"; + const lang = Languages.find(({ code }) => `/${code}/` === url.substring(0, 4))?.code || "en"; sitemap.write( { url, diff --git a/web/src/_entry/app.tsx b/web/src/_entry/app.tsx index b705d3332..4d36eeb80 100644 --- a/web/src/_entry/app.tsx +++ b/web/src/_entry/app.tsx @@ -4,12 +4,12 @@ import { HelmetProvider } from "react-helmet-async"; import { BrowserRouter, Route, RouteProps, Routes } from "react-router-dom"; import { Footer, FooterProps } from "src/components/footer"; import { Loadable } from "src/components/loadable"; -import { Languages } from "src/components/locale/languages"; import { TopBar, TopBarProps } from "src/components/top-bar"; import { StoreProvider } from "src/redux/store"; import { getInitialLanguageCode } from "src/utils/website-language"; import React from "react"; import { Search } from "src/components/search"; +import { DefaultLanguage } from "@dzcode.io/models/dist/language"; let routes: Array< RouteProps & { @@ -56,7 +56,7 @@ let routes: Array< ]; const initialLanguageCode = getInitialLanguageCode(); -if (initialLanguageCode !== Languages[0].code) { +if (initialLanguageCode !== DefaultLanguage.code) { routes = routes.map((route) => { return { ...route, diff --git a/web/src/components/link.tsx b/web/src/components/link.tsx index 7f631c403..0d6a4ffc0 100644 --- a/web/src/components/link.tsx +++ b/web/src/components/link.tsx @@ -1,11 +1,10 @@ +import { DefaultLanguage } from "@dzcode.io/models/dist/language"; import React from "react"; import type { PropsWithChildren } from "react"; import type { LinkProps as RRLinkProps } from "react-router-dom"; import { Link as RRLink } from "react-router-dom"; import { getInitialLanguageCode } from "src/utils/website-language"; -import { Languages } from "./locale/languages"; - interface LinkProps extends Omit { href?: string; } @@ -13,7 +12,7 @@ interface LinkProps extends Omit { const initialLanguageCode = getInitialLanguageCode(); export function Link({ href = "/", ...props }: PropsWithChildren): JSX.Element { - if (href.startsWith("/") && initialLanguageCode !== Languages[0].code) { + if (href.startsWith("/") && initialLanguageCode !== DefaultLanguage.code) { href = `/${initialLanguageCode}${href}`; } return ; diff --git a/web/src/components/locale/languages.ts b/web/src/components/locale/languages.ts deleted file mode 100644 index a1764fe1a..000000000 --- a/web/src/components/locale/languages.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const Languages = [ - { code: "en", label: "English" }, - { code: "ar", label: "العربية" }, -] as const; - -export type Language = (typeof Languages)[number]; diff --git a/web/src/components/redirect.tsx b/web/src/components/redirect.tsx index 367f179bc..78fe1cafb 100644 --- a/web/src/components/redirect.tsx +++ b/web/src/components/redirect.tsx @@ -1,11 +1,10 @@ +import { DefaultLanguage } from "@dzcode.io/models/dist/language"; import React from "react"; import type { PropsWithChildren } from "react"; import type { NavigateProps } from "react-router-dom"; import { Navigate } from "react-router-dom"; import { getInitialLanguageCode } from "src/utils/website-language"; -import { Languages } from "./locale/languages"; - interface RedirectProps extends Omit { href?: string; } @@ -13,7 +12,7 @@ interface RedirectProps extends Omit { const initialLanguageCode = getInitialLanguageCode(); export function Redirect({ href = "/", ...props }: PropsWithChildren): JSX.Element { - if (href.startsWith("/") && initialLanguageCode !== Languages[0].code) { + if (href.startsWith("/") && initialLanguageCode !== DefaultLanguage.code) { href = `/${initialLanguageCode}${href}`; } return ; diff --git a/web/src/components/top-bar.tsx b/web/src/components/top-bar.tsx index 4fcf6eddb..b8b2f00c8 100644 --- a/web/src/components/top-bar.tsx +++ b/web/src/components/top-bar.tsx @@ -10,9 +10,8 @@ import { DictionaryKeys } from "src/components/locale/dictionary"; import { changeLanguage } from "src/redux/actions/settings"; import { useAppSelector } from "src/redux/store"; import { stripLanguageCodeFromHRef } from "src/utils/website-language"; - -import { Language, Languages } from "./locale/languages"; import { useSearchModal } from "src/utils/search-modal"; +import { Language, Languages } from "@dzcode.io/models/dist/language"; export interface TopBarProps { version: string; diff --git a/web/src/redux/actions/settings.ts b/web/src/redux/actions/settings.ts index 0912be8d2..7cd63f07b 100644 --- a/web/src/redux/actions/settings.ts +++ b/web/src/redux/actions/settings.ts @@ -1,7 +1,8 @@ +import { Languages } from "@dzcode.io/models/dist/language"; +import { LanguageCode } from "@dzcode.io/utils/dist/language"; import { captureException } from "@sentry/react"; -import { Language, Languages } from "src/components/locale/languages"; -export const changeLanguage = (languageCode: Language["code"]) => { +export const changeLanguage = (languageCode: LanguageCode) => { let newPath = window.location.pathname; const language = Languages.find(({ code }) => code === languageCode); if (!language) { diff --git a/web/src/redux/slices/settings.ts b/web/src/redux/slices/settings.ts index 43dda2a4d..a5bc5d93c 100644 --- a/web/src/redux/slices/settings.ts +++ b/web/src/redux/slices/settings.ts @@ -1,10 +1,10 @@ +import { LanguageCode } from "@dzcode.io/utils/dist/language"; import { createSlice } from "@reduxjs/toolkit"; -import { Language } from "src/components/locale/languages"; import { getInitialLanguageCode } from "src/utils/website-language"; // ts-prune-ignore-next export interface SettingsState { - readonly languageCode: Language["code"]; + readonly languageCode: LanguageCode; } const initialState: SettingsState = { diff --git a/web/src/utils/fetch.ts b/web/src/utils/fetch.ts index 707984c6f..99dc67dc3 100644 --- a/web/src/utils/fetch.ts +++ b/web/src/utils/fetch.ts @@ -1,5 +1,6 @@ import { Endpoints } from "@dzcode.io/api/dist/app/endpoints"; import { fullstackConfig } from "src/utils/config"; import { fetchV2Factory } from "@dzcode.io/utils/dist/fetch/factory"; +import { getInitialLanguageCode } from "./website-language"; -export const fetchV2 = fetchV2Factory(fullstackConfig); +export const fetchV2 = fetchV2Factory(fullstackConfig, getInitialLanguageCode()); diff --git a/web/src/utils/website-language.ts b/web/src/utils/website-language.ts index 722641fde..448709a8e 100644 --- a/web/src/utils/website-language.ts +++ b/web/src/utils/website-language.ts @@ -1,7 +1,8 @@ -import { Language, Languages } from "src/components/locale/languages"; +import { Languages } from "@dzcode.io/models/dist/language"; +import { LanguageCode } from "@dzcode.io/utils/dist/language"; -let initialLanguageCode: Language["code"] | null = null; -export function getInitialLanguageCode(): Language["code"] { +let initialLanguageCode: LanguageCode | null = null; +export function getInitialLanguageCode(): LanguageCode { if (!initialLanguageCode) { const language = Languages.find(({ code }) => window.location.pathname.startsWith(`/${code}`)) || Languages[0]; From e0402190a6c25ec43defa03a38b95a8243c5f866 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Tue, 31 Dec 2024 23:51:26 +0100 Subject: [PATCH 3/8] localized contribution.title field --- .../migrations/0004_powerful_grandmaster.sql | 3 + api/db/migrations/meta/0004_snapshot.json | 476 ++++++++++++++++++ api/db/migrations/meta/_journal.json | 7 + api/src/contribution/controller.ts | 9 +- api/src/contribution/repository.ts | 20 +- api/src/contribution/table.ts | 4 +- api/src/contribution/types.ts | 10 +- api/src/contributor/controller.ts | 2 +- api/src/contributor/table.ts | 3 +- api/src/contributor/types.ts | 4 +- api/src/digest/cron.ts | 8 +- api/src/project/controller.ts | 2 +- api/src/project/types.ts | 4 +- packages/models/src/contribution/index.ts | 6 +- web/cloudflare/handler/contribution.ts | 7 +- web/src/utils/contribution.ts | 4 +- 16 files changed, 534 insertions(+), 35 deletions(-) create mode 100644 api/db/migrations/0004_powerful_grandmaster.sql create mode 100644 api/db/migrations/meta/0004_snapshot.json diff --git a/api/db/migrations/0004_powerful_grandmaster.sql b/api/db/migrations/0004_powerful_grandmaster.sql new file mode 100644 index 000000000..d548d6e5b --- /dev/null +++ b/api/db/migrations/0004_powerful_grandmaster.sql @@ -0,0 +1,3 @@ +ALTER TABLE "contributions" RENAME COLUMN "title" TO "title_en";--> statement-breakpoint +ALTER TABLE "contributors" ALTER COLUMN "name_ar" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "contributions" ADD COLUMN "title_ar" text DEFAULT '' NOT NULL; \ No newline at end of file diff --git a/api/db/migrations/meta/0004_snapshot.json b/api/db/migrations/meta/0004_snapshot.json new file mode 100644 index 000000000..56a0139b1 --- /dev/null +++ b/api/db/migrations/meta/0004_snapshot.json @@ -0,0 +1,476 @@ +{ + "id": "f0b54b83-6346-44f2-9bb8-cc80498637f8", + "prevId": "be25da33-1037-4e37-8e46-a3ac74a225ad", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.contributions": { + "name": "contributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title_ar": { + "name": "title_ar", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "title_en": { + "name": "title_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_count": { + "name": "activity_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributions_repository_id_repositories_id_fk": { + "name": "contributions_repository_id_repositories_id_fk", + "tableFrom": "contributions", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributions_contributor_id_contributors_id_fk": { + "name": "contributions_contributor_id_contributors_id_fk", + "tableFrom": "contributions", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributions_url_unique": { + "name": "contributions_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.contributor_repository_relation": { + "name": "contributor_repository_relation", + "schema": "", + "columns": { + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributor_repository_relation_contributor_id_contributors_id_fk": { + "name": "contributor_repository_relation_contributor_id_contributors_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributor_repository_relation_repository_id_repositories_id_fk": { + "name": "contributor_repository_relation_repository_id_repositories_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contributor_repository_relation_pk": { + "name": "contributor_repository_relation_pk", + "columns": [ + "contributor_id", + "repository_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.contributors": { + "name": "contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_ar": { + "name": "name_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributors_url_unique": { + "name": "contributors_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.project_tag_relation": { + "name": "project_tag_relation", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_tag_relation_project_id_projects_id_fk": { + "name": "project_tag_relation_project_id_projects_id_fk", + "tableFrom": "project_tag_relation", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_tag_relation_pk": { + "name": "project_tag_relation_pk", + "columns": [ + "project_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "repositories_project_id_projects_id_fk": { + "name": "repositories_project_id_projects_id_fk", + "tableFrom": "repositories", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_provider_owner_name_unique": { + "name": "repositories_provider_owner_name_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "owner", + "name" + ] + } + } + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/db/migrations/meta/_journal.json b/api/db/migrations/meta/_journal.json index 95692df37..f29d2ffda 100644 --- a/api/db/migrations/meta/_journal.json +++ b/api/db/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1735672300337, "tag": "0003_flaky_johnny_blaze", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1735683882031, + "tag": "0004_powerful_grandmaster", + "breakpoints": true } ] } \ No newline at end of file diff --git a/api/src/contribution/controller.ts b/api/src/contribution/controller.ts index b74437022..ae39d75cd 100644 --- a/api/src/contribution/controller.ts +++ b/api/src/contribution/controller.ts @@ -27,8 +27,10 @@ export class ContributionController { } @Get("/for-sitemap") - public async getContributionsForSitemap(): Promise { - const contributions = await this.contributionRepository.findForSitemap(); + public async getContributionsForSitemap( + @QueryParams() { lang }: LanguageQuery, + ): Promise { + const contributions = await this.contributionRepository.findForSitemap(lang); return { contributions, @@ -50,8 +52,9 @@ export class ContributionController { @Get("/:id/title") public async getContributionTitle( @Param("id") id: string, + @QueryParams() { lang }: LanguageQuery, ): Promise { - const contribution = await this.contributionRepository.findTitle(id); + const contribution = await this.contributionRepository.findTitle(id, lang); if (!contribution) throw new NotFoundError("Contribution not found"); diff --git a/api/src/contribution/repository.ts b/api/src/contribution/repository.ts index 36db5d477..b468881b6 100644 --- a/api/src/contribution/repository.ts +++ b/api/src/contribution/repository.ts @@ -15,11 +15,11 @@ import { LanguageCode } from "@dzcode.io/utils/dist/language"; export class ContributionRepository { constructor(private readonly postgresService: PostgresService) {} - public async findTitle(contributionId: string) { + public async findTitle(contributionId: string, lang: LanguageCode) { // todo-ZM: guard against SQL injections in all sql`` statements const statement = sql` SELECT - ${contributionsTable.title} + ${contributionsTable[`title_${lang}`]} as title FROM ${contributionsTable} WHERE @@ -37,11 +37,11 @@ export class ContributionRepository { return camelCased; } - public async findForProject(projectId: string) { + public async findForProject(projectId: string, lang: LanguageCode) { const statement = sql` SELECT ${contributionsTable.id}, - ${contributionsTable.title} + ${contributionsTable[`title_${lang}`]} as title FROM ${contributionsTable} INNER JOIN @@ -59,11 +59,11 @@ export class ContributionRepository { return camelCased; } - public async findForContributor(contributorId: string) { + public async findForContributor(contributorId: string, lang: LanguageCode) { const statement = sql` SELECT ${contributionsTable.id}, - ${contributionsTable.title} + ${contributionsTable[`title_${lang}`]} as title FROM ${contributionsTable} INNER JOIN @@ -81,11 +81,11 @@ export class ContributionRepository { return camelCased; } - public async findForSitemap() { + public async findForSitemap(lang: LanguageCode) { const statement = sql` SELECT ${contributionsTable.id}, - ${contributionsTable.title} + ${contributionsTable[`title_${lang}`]} as title FROM ${contributionsTable} `; @@ -133,7 +133,7 @@ export class ContributionRepository { 'id', c.id, 'title', - c.title, + c.title_${sql.raw(lang)}, 'type', c.type, 'url', @@ -207,7 +207,7 @@ export class ContributionRepository { 'id', c.id, 'title', - c.title, + c.title_${sql.raw(lang)}, 'type', c.type, 'url', diff --git a/api/src/contribution/table.ts b/api/src/contribution/table.ts index 07b49ea65..2eff894e9 100644 --- a/api/src/contribution/table.ts +++ b/api/src/contribution/table.ts @@ -9,7 +9,9 @@ export const contributionsTable = pgTable("contributions", { recordImportedAt: text("record_imported_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), - title: text("title").notNull(), + // todo: remove default value after migration + title_ar: text("title_ar").notNull().default(""), + title_en: text("title_en").notNull(), updatedAt: text("updated_at").notNull(), url: text("url").notNull().unique(), type: text("type").notNull().$type(), diff --git a/api/src/contribution/types.ts b/api/src/contribution/types.ts index e020b7aea..9c46dd284 100644 --- a/api/src/contribution/types.ts +++ b/api/src/contribution/types.ts @@ -1,4 +1,4 @@ -import { ContributionEntity } from "@dzcode.io/models/dist/contribution"; +import { ContributionNoLang } from "@dzcode.io/models/dist/contribution"; import { ContributorNoLang } from "@dzcode.io/models/dist/contributor"; import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; @@ -6,7 +6,7 @@ import { GeneralResponse } from "src/app/types"; export interface GetContributionsResponse extends GeneralResponse { contributions: Array< - Pick & { + Pick & { repository: Pick & { project: Pick; }; @@ -17,7 +17,7 @@ export interface GetContributionsResponse extends GeneralResponse { export interface GetContributionResponse extends GeneralResponse { contribution: Pick< - ContributionEntity, + ContributionNoLang, "id" | "title" | "type" | "url" | "updatedAt" | "activityCount" > & { repository: Pick & { @@ -28,9 +28,9 @@ export interface GetContributionResponse extends GeneralResponse { } export interface GetContributionTitleResponse extends GeneralResponse { - contribution: Pick; + contribution: Pick; } export interface GetContributionsForSitemapResponse extends GeneralResponse { - contributions: Array>; + contributions: Array>; } diff --git a/api/src/contributor/controller.ts b/api/src/contributor/controller.ts index 0b9811106..b548d3a9d 100644 --- a/api/src/contributor/controller.ts +++ b/api/src/contributor/controller.ts @@ -49,7 +49,7 @@ export class ContributorController { const [contributor, projects, contributions] = await Promise.all([ this.contributorRepository.findWithStats(id, lang), this.projectRepository.findForContributor(id), - this.contributionRepository.findForContributor(id), + this.contributionRepository.findForContributor(id, lang), ]); if (!contributor) throw new NotFoundError("Contributor not found"); diff --git a/api/src/contributor/table.ts b/api/src/contributor/table.ts index 3eb5d4264..370738a1b 100644 --- a/api/src/contributor/table.ts +++ b/api/src/contributor/table.ts @@ -9,8 +9,7 @@ export const contributorsTable = pgTable("contributors", { .notNull() .default(sql`CURRENT_TIMESTAMP`), runId: text("run_id").notNull(), - // todo: remove default value after migration - name_ar: text("name_ar").notNull().default(""), + name_ar: text("name_ar").notNull(), name_en: text("name_en").notNull(), username: text("username").notNull(), url: text("url").notNull().unique(), diff --git a/api/src/contributor/types.ts b/api/src/contributor/types.ts index 4d510f2cf..d51ffdf83 100644 --- a/api/src/contributor/types.ts +++ b/api/src/contributor/types.ts @@ -1,4 +1,4 @@ -import { ContributionEntity } from "@dzcode.io/models/dist/contribution"; +import { ContributionNoLang } from "@dzcode.io/models/dist/contribution"; import { ContributorNoLang } from "@dzcode.io/models/dist/contributor"; import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { GeneralResponse } from "src/app/types"; @@ -30,7 +30,7 @@ export interface GetContributorResponse extends GeneralResponse { ranking: number; } >; - contributions: Array>; + contributions: Array>; }; } diff --git a/api/src/digest/cron.ts b/api/src/digest/cron.ts index 17e89fd84..18819a4bf 100644 --- a/api/src/digest/cron.ts +++ b/api/src/digest/cron.ts @@ -154,8 +154,14 @@ export class DigestCron { }); const type = issue.pull_request ? "PULL_REQUEST" : "ISSUE"; + + // todo: call AIService + const title_en = issue.title; + const title_ar = `ar ${title_en}`; + const contributionEntity: ContributionRow = { - title: issue.title, + title_en, + title_ar, type, updatedAt: issue.updated_at, activityCount: issue.comments, diff --git a/api/src/project/controller.ts b/api/src/project/controller.ts index afa514d42..b88583b6b 100644 --- a/api/src/project/controller.ts +++ b/api/src/project/controller.ts @@ -50,7 +50,7 @@ export class ProjectController { this.projectRepository.findWithStats(id), this.repositoryRepository.findForProject(id), this.contributorRepository.findForProject(id, lang), - this.contributionRepository.findForProject(id), + this.contributionRepository.findForProject(id, lang), ]); if (!project) throw new NotFoundError("Project not found"); diff --git a/api/src/project/types.ts b/api/src/project/types.ts index 7eb631f1a..953656271 100644 --- a/api/src/project/types.ts +++ b/api/src/project/types.ts @@ -1,4 +1,4 @@ -import { ContributionEntity } from "@dzcode.io/models/dist/contribution"; +import { ContributionNoLang } from "@dzcode.io/models/dist/contribution"; import { ContributorNoLang } from "@dzcode.io/models/dist/contributor"; import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; @@ -35,7 +35,7 @@ export interface GetProjectResponse extends GeneralResponse { score: number; } >; - contributions: Array>; + contributions: Array>; totalRepoContributorCount: number; repoCount: number; diff --git a/packages/models/src/contribution/index.ts b/packages/models/src/contribution/index.ts index c493210db..9c557c05d 100644 --- a/packages/models/src/contribution/index.ts +++ b/packages/models/src/contribution/index.ts @@ -1,9 +1,13 @@ +import { StripLanguage } from "@dzcode.io/utils/dist/ts"; import { BaseEntity } from "src/_base"; export type ContributionEntity = BaseEntity & { - title: string; + title_ar: string; + title_en: string; type: "ISSUE" | "PULL_REQUEST"; url: string; updatedAt: string; activityCount: number; }; + +export type ContributionNoLang = StripLanguage; diff --git a/web/cloudflare/handler/contribution.ts b/web/cloudflare/handler/contribution.ts index d07b975bd..9800071e7 100644 --- a/web/cloudflare/handler/contribution.ts +++ b/web/cloudflare/handler/contribution.ts @@ -9,9 +9,9 @@ import { Environment, environments } from "@dzcode.io/utils/dist/config/environm import { fsConfig } from "@dzcode.io/utils/dist/config"; import { plainLocalize } from "@dzcode.io/web/dist/components/locale/utils"; import { dictionary, AllDictionaryKeys } from "@dzcode.io/web/dist/components/locale/dictionary"; -import { LanguageEntity } from "@dzcode.io/models/dist/language"; import { fetchV2Factory } from "@dzcode.io/utils/dist/fetch/factory"; import { Endpoints } from "@dzcode.io/api/dist/app/endpoints"; +import { LanguageCode } from "@dzcode.io/utils/dist/language"; export interface Env { STAGE: Environment; @@ -27,8 +27,7 @@ export const handleContributionRequest: PagesFunction = async (context) => const pathName = new URL(context.request.url).pathname; const languageRegex = /^\/(ar|en)\//i; - const language = (pathName?.match(languageRegex)?.[1]?.toLowerCase() || - "en") as LanguageEntity["code"]; + const language = (pathName?.match(languageRegex)?.[1]?.toLowerCase() || "en") as LanguageCode; const notFound = language === "ar" ? notFoundAr : notFoundEn; const contributionIdRegex = /contribute\/(.*)-(.*)-(.*)/; @@ -45,7 +44,7 @@ export const handleContributionRequest: PagesFunction = async (context) => plainLocalize(dictionary, language, key, "NO-TRANSLATION"); const fullstackConfig = fsConfig(stage); - const fetchV2 = fetchV2Factory(fullstackConfig); + const fetchV2 = fetchV2Factory(fullstackConfig, language); try { const { contribution } = await fetchV2("api:contributions/:id/title", { diff --git a/web/src/utils/contribution.ts b/web/src/utils/contribution.ts index 390c4b44a..9f06e2b27 100644 --- a/web/src/utils/contribution.ts +++ b/web/src/utils/contribution.ts @@ -1,8 +1,8 @@ -import { ContributionEntity } from "@dzcode.io/models/dist/contribution"; +import { ContributionNoLang } from "@dzcode.io/models/dist/contribution"; export function getContributionURL({ id, title, -}: Pick): string { +}: Pick): string { return `/contribute/${title.replace(/\s/g, "-")}-${id}`; } From 4011e4547e20becd56f3848f93f8468e193f1d02 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Wed, 1 Jan 2025 00:37:32 +0100 Subject: [PATCH 4/8] localized project.name field --- api/db/migrations/0005_bizarre_piledriver.sql | 3 + api/db/migrations/meta/0005_snapshot.json | 482 ++++++++++++++++++ api/db/migrations/meta/_journal.json | 7 + api/src/contribution/repository.ts | 4 +- api/src/contribution/table.ts | 3 +- api/src/contribution/types.ts | 6 +- api/src/contributor/controller.ts | 2 +- api/src/contributor/types.ts | 4 +- api/src/digest/cron.ts | 7 +- api/src/project/controller.ts | 13 +- api/src/project/repository.ts | 17 +- api/src/project/table.ts | 4 +- api/src/project/types.ts | 10 +- packages/models/src/project/index.ts | 6 +- web/cloudflare/handler/project.ts | 7 +- 15 files changed, 540 insertions(+), 35 deletions(-) create mode 100644 api/db/migrations/0005_bizarre_piledriver.sql create mode 100644 api/db/migrations/meta/0005_snapshot.json diff --git a/api/db/migrations/0005_bizarre_piledriver.sql b/api/db/migrations/0005_bizarre_piledriver.sql new file mode 100644 index 000000000..dcefec38c --- /dev/null +++ b/api/db/migrations/0005_bizarre_piledriver.sql @@ -0,0 +1,3 @@ +ALTER TABLE "projects" RENAME COLUMN "name" TO "name_en";--> statement-breakpoint +ALTER TABLE "contributions" ALTER COLUMN "title_ar" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "projects" ADD COLUMN "name_ar" text DEFAULT '' NOT NULL; \ No newline at end of file diff --git a/api/db/migrations/meta/0005_snapshot.json b/api/db/migrations/meta/0005_snapshot.json new file mode 100644 index 000000000..a3d1f11fe --- /dev/null +++ b/api/db/migrations/meta/0005_snapshot.json @@ -0,0 +1,482 @@ +{ + "id": "1483b75c-2c68-498c-bc1b-ea3e8b96284a", + "prevId": "f0b54b83-6346-44f2-9bb8-cc80498637f8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.contributions": { + "name": "contributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title_ar": { + "name": "title_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_en": { + "name": "title_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_count": { + "name": "activity_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributions_repository_id_repositories_id_fk": { + "name": "contributions_repository_id_repositories_id_fk", + "tableFrom": "contributions", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributions_contributor_id_contributors_id_fk": { + "name": "contributions_contributor_id_contributors_id_fk", + "tableFrom": "contributions", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributions_url_unique": { + "name": "contributions_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.contributor_repository_relation": { + "name": "contributor_repository_relation", + "schema": "", + "columns": { + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributor_repository_relation_contributor_id_contributors_id_fk": { + "name": "contributor_repository_relation_contributor_id_contributors_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributor_repository_relation_repository_id_repositories_id_fk": { + "name": "contributor_repository_relation_repository_id_repositories_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contributor_repository_relation_pk": { + "name": "contributor_repository_relation_pk", + "columns": [ + "contributor_id", + "repository_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.contributors": { + "name": "contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_ar": { + "name": "name_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributors_url_unique": { + "name": "contributors_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.project_tag_relation": { + "name": "project_tag_relation", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_tag_relation_project_id_projects_id_fk": { + "name": "project_tag_relation_project_id_projects_id_fk", + "tableFrom": "project_tag_relation", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_tag_relation_pk": { + "name": "project_tag_relation_pk", + "columns": [ + "project_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "name_ar": { + "name": "name_ar", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "name_en": { + "name": "name_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "repositories_project_id_projects_id_fk": { + "name": "repositories_project_id_projects_id_fk", + "tableFrom": "repositories", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_provider_owner_name_unique": { + "name": "repositories_provider_owner_name_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "owner", + "name" + ] + } + } + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/db/migrations/meta/_journal.json b/api/db/migrations/meta/_journal.json index f29d2ffda..b1a011622 100644 --- a/api/db/migrations/meta/_journal.json +++ b/api/db/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1735683882031, "tag": "0004_powerful_grandmaster", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1735686453611, + "tag": "0005_bizarre_piledriver", + "breakpoints": true } ] } \ No newline at end of file diff --git a/api/src/contribution/repository.ts b/api/src/contribution/repository.ts index b468881b6..273879975 100644 --- a/api/src/contribution/repository.ts +++ b/api/src/contribution/repository.ts @@ -118,7 +118,7 @@ export class ContributionRepository { const statement = sql` SELECT p.id as id, - p.name as name, + p.name_${sql.raw(lang)} as name, json_agg( json_build_object('id', r.id, 'name', r.name, 'owner', r.owner, 'contributions', r.contributions) ) AS repositories @@ -192,7 +192,7 @@ export class ContributionRepository { const statement = sql` SELECT p.id as id, - p.name as name, + p.name_${sql.raw(lang)} as name, json_agg( json_build_object('id', r.id, 'name', r.name, 'owner', r.owner, 'contributions', r.contributions) ) AS repositories diff --git a/api/src/contribution/table.ts b/api/src/contribution/table.ts index 2eff894e9..388cfd514 100644 --- a/api/src/contribution/table.ts +++ b/api/src/contribution/table.ts @@ -9,8 +9,7 @@ export const contributionsTable = pgTable("contributions", { recordImportedAt: text("record_imported_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), - // todo: remove default value after migration - title_ar: text("title_ar").notNull().default(""), + title_ar: text("title_ar").notNull(), title_en: text("title_en").notNull(), updatedAt: text("updated_at").notNull(), url: text("url").notNull().unique(), diff --git a/api/src/contribution/types.ts b/api/src/contribution/types.ts index 9c46dd284..eaada4bd6 100644 --- a/api/src/contribution/types.ts +++ b/api/src/contribution/types.ts @@ -1,6 +1,6 @@ import { ContributionNoLang } from "@dzcode.io/models/dist/contribution"; import { ContributorNoLang } from "@dzcode.io/models/dist/contributor"; -import { ProjectEntity } from "@dzcode.io/models/dist/project"; +import { ProjectNoLang } from "@dzcode.io/models/dist/project"; import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; import { GeneralResponse } from "src/app/types"; @@ -8,7 +8,7 @@ export interface GetContributionsResponse extends GeneralResponse { contributions: Array< Pick & { repository: Pick & { - project: Pick; + project: Pick; }; contributor: Pick; } @@ -21,7 +21,7 @@ export interface GetContributionResponse extends GeneralResponse { "id" | "title" | "type" | "url" | "updatedAt" | "activityCount" > & { repository: Pick & { - project: Pick; + project: Pick; }; contributor: Pick; }; diff --git a/api/src/contributor/controller.ts b/api/src/contributor/controller.ts index b548d3a9d..19b86ca35 100644 --- a/api/src/contributor/controller.ts +++ b/api/src/contributor/controller.ts @@ -48,7 +48,7 @@ export class ContributorController { ): Promise { const [contributor, projects, contributions] = await Promise.all([ this.contributorRepository.findWithStats(id, lang), - this.projectRepository.findForContributor(id), + this.projectRepository.findForContributor(id, lang), this.contributionRepository.findForContributor(id, lang), ]); diff --git a/api/src/contributor/types.ts b/api/src/contributor/types.ts index d51ffdf83..a4fb0dc13 100644 --- a/api/src/contributor/types.ts +++ b/api/src/contributor/types.ts @@ -1,6 +1,6 @@ import { ContributionNoLang } from "@dzcode.io/models/dist/contribution"; import { ContributorNoLang } from "@dzcode.io/models/dist/contributor"; -import { ProjectEntity } from "@dzcode.io/models/dist/project"; +import { ProjectNoLang } from "@dzcode.io/models/dist/project"; import { GeneralResponse } from "src/app/types"; export interface GetContributorsForSitemapResponse extends GeneralResponse { @@ -23,7 +23,7 @@ export interface GetContributorResponse extends GeneralResponse { totalContributionScore: number; totalRepositoryCount: number; projects: Array< - Pick & { + Pick & { totalRepoContributorCount: number; totalRepoScore: number; totalRepoStars: number; diff --git a/api/src/digest/cron.ts b/api/src/digest/cron.ts index 18819a4bf..9b584a10c 100644 --- a/api/src/digest/cron.ts +++ b/api/src/digest/cron.ts @@ -82,10 +82,15 @@ export class DigestCron { // if (Math.random()) return; for (const project of projectsFromDataFolder) { + // todo: call AIService + const name_en = project.name; + const name_ar = `ar ${name_en}`; + const projectEntity: ProjectRow = { runId, id: project.slug.replace(/[.]/g, "-"), // NOTE-OB: MeiliSearch doesn't allow dots in ids - name: project.name, + name_en, + name_ar, }; const [{ id: projectId }] = await this.projectsRepository.upsert(projectEntity); for (const tagId of project.tags || []) { diff --git a/api/src/project/controller.ts b/api/src/project/controller.ts index b88583b6b..e2236c571 100644 --- a/api/src/project/controller.ts +++ b/api/src/project/controller.ts @@ -24,8 +24,8 @@ export class ProjectController { ) {} @Get("/") - public async getProjects(): Promise { - const projects = await this.projectRepository.findForList(); + public async getProjects(@QueryParams() { lang }: LanguageQuery): Promise { + const projects = await this.projectRepository.findForList(lang); return { projects, @@ -47,7 +47,7 @@ export class ProjectController { @QueryParams() { lang }: LanguageQuery, ): Promise { const [project, repositories, contributors, contributions] = await Promise.all([ - this.projectRepository.findWithStats(id), + this.projectRepository.findWithStats(id, lang), this.repositoryRepository.findForProject(id), this.contributorRepository.findForProject(id, lang), this.contributionRepository.findForProject(id, lang), @@ -66,8 +66,11 @@ export class ProjectController { } @Get("/:id/name") - public async getProjectName(@Param("id") id: string): Promise { - const project = await this.projectRepository.findName(id); + public async getProjectName( + @Param("id") id: string, + @QueryParams() { lang }: LanguageQuery, + ): Promise { + const project = await this.projectRepository.findName(id, lang); if (!project) throw new NotFoundError("Project not found"); diff --git a/api/src/project/repository.ts b/api/src/project/repository.ts index 3e47e56a9..d8bd2b3f9 100644 --- a/api/src/project/repository.ts +++ b/api/src/project/repository.ts @@ -7,16 +7,17 @@ import { PostgresService } from "src/postgres/service"; import { Service } from "typedi"; import { ProjectRow, projectsTable, ProjectTagRelationRow, projectTagRelationTable } from "./table"; +import { LanguageCode } from "@dzcode.io/utils/dist/language"; @Service() export class ProjectRepository { constructor(private readonly postgresService: PostgresService) {} - public async findName(projectId: string) { + public async findName(projectId: string, lang: LanguageCode) { const statement = sql` SELECT ${projectsTable.id}, - ${projectsTable.name} + ${projectsTable[`name_${lang}`]} as name FROM ${projectsTable} WHERE @@ -34,11 +35,11 @@ export class ProjectRepository { return camelCased; } - public async findWithStats(projectId: string) { + public async findWithStats(projectId: string, lang: LanguageCode) { const statement = sql` SELECT id, - name, + name_${sql.raw(lang)} as name, sum(repo_with_stats.contributor_count)::int as total_repo_contributor_count, sum(repo_with_stats.stars)::int as total_repo_stars, sum(repo_with_stats.score)::int as total_repo_score, @@ -77,11 +78,11 @@ export class ProjectRepository { return camelCased; } - public async findForList() { + public async findForList(lang: LanguageCode) { const statement = sql` SELECT p.id, - p.name, + p.name_${sql.raw(lang)} as name, sum(repo_with_stats.contributor_count)::int as total_repo_contributor_count, sum(repo_with_stats.stars)::int as total_repo_stars, sum(repo_with_stats.score)::int as total_repo_score, @@ -122,11 +123,11 @@ export class ProjectRepository { return camelCased; } - public async findForContributor(id: string) { + public async findForContributor(id: string, lang: LanguageCode) { const statement = sql` SELECT id, - name, + name_${sql.raw(lang)} as name, sum(repo_with_stats.contributor_count)::int as total_repo_contributor_count, sum(repo_with_stats.stars)::int as total_repo_stars, sum(repo_with_stats.score)::int as total_repo_score, diff --git a/api/src/project/table.ts b/api/src/project/table.ts index 7de9603ae..767b66d44 100644 --- a/api/src/project/table.ts +++ b/api/src/project/table.ts @@ -7,7 +7,9 @@ export const projectsTable = pgTable("projects", { recordImportedAt: text("record_imported_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), - name: text("name").notNull(), + // todo: remove default value after migration + name_ar: text("name_ar").notNull().default(""), + name_en: text("name_en").notNull(), runId: text("run_id").notNull(), }); diff --git a/api/src/project/types.ts b/api/src/project/types.ts index 953656271..d2d0e95fb 100644 --- a/api/src/project/types.ts +++ b/api/src/project/types.ts @@ -1,17 +1,17 @@ import { ContributionNoLang } from "@dzcode.io/models/dist/contribution"; import { ContributorNoLang } from "@dzcode.io/models/dist/contributor"; -import { ProjectEntity } from "@dzcode.io/models/dist/project"; +import { ProjectNoLang } from "@dzcode.io/models/dist/project"; import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; import { TagEntity } from "@dzcode.io/models/dist/tag"; import { GeneralResponse } from "src/app/types"; export interface GetProjectsForSitemapResponse extends GeneralResponse { - projects: Array>; + projects: Array>; } export interface GetProjectsResponse extends GeneralResponse { projects: Array< - Pick & { + Pick & { totalRepoContributorCount: number; totalRepoScore: number; totalRepoStars: number; @@ -22,7 +22,7 @@ export interface GetProjectsResponse extends GeneralResponse { } export interface GetProjectResponse extends GeneralResponse { - project: Omit & { + project: Omit & { repositories: Array< Omit & { contributorCount: number; @@ -45,5 +45,5 @@ export interface GetProjectResponse extends GeneralResponse { } export interface GetProjectNameResponse extends GeneralResponse { - project: Pick; + project: Pick; } diff --git a/packages/models/src/project/index.ts b/packages/models/src/project/index.ts index 92973da32..594ac931f 100644 --- a/packages/models/src/project/index.ts +++ b/packages/models/src/project/index.ts @@ -1,5 +1,9 @@ +import { StripLanguage } from "@dzcode.io/utils/dist/ts"; import { BaseEntity } from "src/_base"; export type ProjectEntity = BaseEntity & { - name: string; + name_ar: string; + name_en: string; }; + +export type ProjectNoLang = StripLanguage; diff --git a/web/cloudflare/handler/project.ts b/web/cloudflare/handler/project.ts index 64cd8d729..7fcf6ea7d 100644 --- a/web/cloudflare/handler/project.ts +++ b/web/cloudflare/handler/project.ts @@ -9,9 +9,9 @@ import { Environment, environments } from "@dzcode.io/utils/dist/config/environm import { fsConfig } from "@dzcode.io/utils/dist/config"; import { plainLocalize } from "@dzcode.io/web/dist/components/locale/utils"; import { dictionary, AllDictionaryKeys } from "@dzcode.io/web/dist/components/locale/dictionary"; -import { LanguageEntity } from "@dzcode.io/models/dist/language"; import { fetchV2Factory } from "@dzcode.io/utils/dist/fetch/factory"; import { Endpoints } from "@dzcode.io/api/dist/app/endpoints"; +import { LanguageCode } from "@dzcode.io/utils/dist/language"; export interface Env { STAGE: Environment; @@ -27,8 +27,7 @@ export const handleProjectRequest: PagesFunction = async (context) => { const pathName = new URL(context.request.url).pathname; const languageRegex = /^\/(ar|en)\//i; - const language = (pathName?.match(languageRegex)?.[1]?.toLowerCase() || - "en") as LanguageEntity["code"]; + const language = (pathName?.match(languageRegex)?.[1]?.toLowerCase() || "en") as LanguageCode; const notFound = language === "ar" ? notFoundAr : notFoundEn; const projectIdRegex = /projects\/(.*)/; @@ -44,7 +43,7 @@ export const handleProjectRequest: PagesFunction = async (context) => { plainLocalize(dictionary, language, key, "NO-TRANSLATION"); const fullstackConfig = fsConfig(stage); - const fetchV2 = fetchV2Factory(fullstackConfig); + const fetchV2 = fetchV2Factory(fullstackConfig, language); try { const { project } = await fetchV2("api:projects/:id/name", { params: { id: projectId } }); From 56afaa21eab851aff50abaf9b3c0143e0e0bc0b4 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Wed, 1 Jan 2025 00:39:44 +0100 Subject: [PATCH 5/8] remove default value for name_ar column in projects table --- .../0006_amazing_stark_industries.sql | 1 + api/db/migrations/meta/0006_snapshot.json | 481 ++++++++++++++++++ api/db/migrations/meta/_journal.json | 7 + api/src/project/table.ts | 3 +- 4 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 api/db/migrations/0006_amazing_stark_industries.sql create mode 100644 api/db/migrations/meta/0006_snapshot.json diff --git a/api/db/migrations/0006_amazing_stark_industries.sql b/api/db/migrations/0006_amazing_stark_industries.sql new file mode 100644 index 000000000..91f51c683 --- /dev/null +++ b/api/db/migrations/0006_amazing_stark_industries.sql @@ -0,0 +1 @@ +ALTER TABLE "projects" ALTER COLUMN "name_ar" DROP DEFAULT; \ No newline at end of file diff --git a/api/db/migrations/meta/0006_snapshot.json b/api/db/migrations/meta/0006_snapshot.json new file mode 100644 index 000000000..ee0e30db8 --- /dev/null +++ b/api/db/migrations/meta/0006_snapshot.json @@ -0,0 +1,481 @@ +{ + "id": "54b5fefc-b314-4ce9-ad6e-be5314d2c4f4", + "prevId": "1483b75c-2c68-498c-bc1b-ea3e8b96284a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.contributions": { + "name": "contributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title_ar": { + "name": "title_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_en": { + "name": "title_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_count": { + "name": "activity_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributions_repository_id_repositories_id_fk": { + "name": "contributions_repository_id_repositories_id_fk", + "tableFrom": "contributions", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributions_contributor_id_contributors_id_fk": { + "name": "contributions_contributor_id_contributors_id_fk", + "tableFrom": "contributions", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributions_url_unique": { + "name": "contributions_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.contributor_repository_relation": { + "name": "contributor_repository_relation", + "schema": "", + "columns": { + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributor_repository_relation_contributor_id_contributors_id_fk": { + "name": "contributor_repository_relation_contributor_id_contributors_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributor_repository_relation_repository_id_repositories_id_fk": { + "name": "contributor_repository_relation_repository_id_repositories_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contributor_repository_relation_pk": { + "name": "contributor_repository_relation_pk", + "columns": [ + "contributor_id", + "repository_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.contributors": { + "name": "contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_ar": { + "name": "name_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributors_url_unique": { + "name": "contributors_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.project_tag_relation": { + "name": "project_tag_relation", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_tag_relation_project_id_projects_id_fk": { + "name": "project_tag_relation_project_id_projects_id_fk", + "tableFrom": "project_tag_relation", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_tag_relation_pk": { + "name": "project_tag_relation_pk", + "columns": [ + "project_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "name_ar": { + "name": "name_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "repositories_project_id_projects_id_fk": { + "name": "repositories_project_id_projects_id_fk", + "tableFrom": "repositories", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_provider_owner_name_unique": { + "name": "repositories_provider_owner_name_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "owner", + "name" + ] + } + } + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/db/migrations/meta/_journal.json b/api/db/migrations/meta/_journal.json index b1a011622..ba1dc9c90 100644 --- a/api/db/migrations/meta/_journal.json +++ b/api/db/migrations/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1735686453611, "tag": "0005_bizarre_piledriver", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1735688289807, + "tag": "0006_amazing_stark_industries", + "breakpoints": true } ] } \ No newline at end of file diff --git a/api/src/project/table.ts b/api/src/project/table.ts index 767b66d44..f7eaa6ba9 100644 --- a/api/src/project/table.ts +++ b/api/src/project/table.ts @@ -7,8 +7,7 @@ export const projectsTable = pgTable("projects", { recordImportedAt: text("record_imported_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), - // todo: remove default value after migration - name_ar: text("name_ar").notNull().default(""), + name_ar: text("name_ar").notNull(), name_en: text("name_en").notNull(), runId: text("run_id").notNull(), }); From b0e1caa65e0f171a0d0aa862dcfc426c6a0f8cf0 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Wed, 1 Jan 2025 01:16:34 +0100 Subject: [PATCH 6/8] uppercased language consts --- packages/models/src/language/index.ts | 5 ++--- web/src/_build/pages/static-pages.ts | 4 ++-- web/src/_build/sitemap.ts | 4 ++-- web/src/_entry/app.tsx | 4 ++-- web/src/components/link.tsx | 4 ++-- web/src/components/redirect.tsx | 4 ++-- web/src/components/top-bar.tsx | 4 ++-- web/src/redux/actions/settings.ts | 8 ++++---- web/src/utils/website-language.ts | 5 +++-- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/models/src/language/index.ts b/packages/models/src/language/index.ts index 0ffa4388b..0ff8a77df 100644 --- a/packages/models/src/language/index.ts +++ b/packages/models/src/language/index.ts @@ -8,10 +8,9 @@ export interface Language { baseUrl: string; } -// todo: renamed to LANGUAGES -export const Languages: Language[] = [ +export const LANGUAGES: Language[] = [ { code: "en", shortLabel: "EN", label: "English", direction: "ltr", baseUrl: "" }, { code: "ar", shortLabel: "ع", label: "العربية", direction: "rtl", baseUrl: "/ar" }, ]; -export const DefaultLanguage: Language = Languages[0]; +export const DEFAULT_LANGUAGE: Language = LANGUAGES[0]; diff --git a/web/src/_build/pages/static-pages.ts b/web/src/_build/pages/static-pages.ts index 4f18b5ecf..38bb2f31d 100644 --- a/web/src/_build/pages/static-pages.ts +++ b/web/src/_build/pages/static-pages.ts @@ -2,7 +2,7 @@ import { plainLocalize } from "../../components/locale/utils"; import { dictionary, AllDictionaryKeys } from "../../components/locale/dictionary"; import { PageInfo, PageInfoWithLocalKeys } from "."; -import { Languages } from "@dzcode.io/models/dist/language"; +import { LANGUAGES } from "@dzcode.io/models/dist/language"; const localize = (key: AllDictionaryKeys, language: string) => plainLocalize(dictionary, language, key, "NO-TRANSLATION"); @@ -61,7 +61,7 @@ const staticURLs: PageInfoWithLocalKeys[] = [ export const staticPages: PageInfo[] = staticURLs.reduce( (acc, { title, description, uri, ...page }) => [ ...acc, - ...Languages.map(({ code }) => ({ + ...LANGUAGES.map(({ code }) => ({ ...page, title: localize(title, code), description: localize(description, code), diff --git a/web/src/_build/sitemap.ts b/web/src/_build/sitemap.ts index 67ef0aa09..f7db4932a 100644 --- a/web/src/_build/sitemap.ts +++ b/web/src/_build/sitemap.ts @@ -2,7 +2,7 @@ import { createWriteStream } from "fs"; import { join } from "path"; import { SitemapStream } from "sitemap"; import { staticPages } from "./pages"; -import { Languages } from "@dzcode.io/models/dist/language"; +import { LANGUAGES } from "@dzcode.io/models/dist/language"; const distFolder = "./bundle"; @@ -23,7 +23,7 @@ writeStream.on("error", (err) => { writeStream.on("ready", () => { sitemap.pipe(writeStream); urls.forEach((url) => { - const lang = Languages.find(({ code }) => `/${code}/` === url.substring(0, 4))?.code || "en"; + const lang = LANGUAGES.find(({ code }) => `/${code}/` === url.substring(0, 4))?.code || "en"; sitemap.write( { url, diff --git a/web/src/_entry/app.tsx b/web/src/_entry/app.tsx index 4d36eeb80..be986b0f5 100644 --- a/web/src/_entry/app.tsx +++ b/web/src/_entry/app.tsx @@ -9,7 +9,7 @@ import { StoreProvider } from "src/redux/store"; import { getInitialLanguageCode } from "src/utils/website-language"; import React from "react"; import { Search } from "src/components/search"; -import { DefaultLanguage } from "@dzcode.io/models/dist/language"; +import { DEFAULT_LANGUAGE } from "@dzcode.io/models/dist/language"; let routes: Array< RouteProps & { @@ -56,7 +56,7 @@ let routes: Array< ]; const initialLanguageCode = getInitialLanguageCode(); -if (initialLanguageCode !== DefaultLanguage.code) { +if (initialLanguageCode !== DEFAULT_LANGUAGE.code) { routes = routes.map((route) => { return { ...route, diff --git a/web/src/components/link.tsx b/web/src/components/link.tsx index 0d6a4ffc0..dcac6d6f9 100644 --- a/web/src/components/link.tsx +++ b/web/src/components/link.tsx @@ -1,4 +1,4 @@ -import { DefaultLanguage } from "@dzcode.io/models/dist/language"; +import { DEFAULT_LANGUAGE } from "@dzcode.io/models/dist/language"; import React from "react"; import type { PropsWithChildren } from "react"; import type { LinkProps as RRLinkProps } from "react-router-dom"; @@ -12,7 +12,7 @@ interface LinkProps extends Omit { const initialLanguageCode = getInitialLanguageCode(); export function Link({ href = "/", ...props }: PropsWithChildren): JSX.Element { - if (href.startsWith("/") && initialLanguageCode !== DefaultLanguage.code) { + if (href.startsWith("/") && initialLanguageCode !== DEFAULT_LANGUAGE.code) { href = `/${initialLanguageCode}${href}`; } return ; diff --git a/web/src/components/redirect.tsx b/web/src/components/redirect.tsx index 78fe1cafb..aebef37eb 100644 --- a/web/src/components/redirect.tsx +++ b/web/src/components/redirect.tsx @@ -1,4 +1,4 @@ -import { DefaultLanguage } from "@dzcode.io/models/dist/language"; +import { DEFAULT_LANGUAGE } from "@dzcode.io/models/dist/language"; import React from "react"; import type { PropsWithChildren } from "react"; import type { NavigateProps } from "react-router-dom"; @@ -12,7 +12,7 @@ interface RedirectProps extends Omit { const initialLanguageCode = getInitialLanguageCode(); export function Redirect({ href = "/", ...props }: PropsWithChildren): JSX.Element { - if (href.startsWith("/") && initialLanguageCode !== DefaultLanguage.code) { + if (href.startsWith("/") && initialLanguageCode !== DEFAULT_LANGUAGE.code) { href = `/${initialLanguageCode}${href}`; } return ; diff --git a/web/src/components/top-bar.tsx b/web/src/components/top-bar.tsx index b8b2f00c8..cccaabce2 100644 --- a/web/src/components/top-bar.tsx +++ b/web/src/components/top-bar.tsx @@ -11,7 +11,7 @@ import { changeLanguage } from "src/redux/actions/settings"; import { useAppSelector } from "src/redux/store"; import { stripLanguageCodeFromHRef } from "src/utils/website-language"; import { useSearchModal } from "src/utils/search-modal"; -import { Language, Languages } from "@dzcode.io/models/dist/language"; +import { Language, LANGUAGES } from "@dzcode.io/models/dist/language"; export interface TopBarProps { version: string; @@ -32,7 +32,7 @@ export function TopBar({ version, links }: TopBarProps): JSX.Element { const { selectedLanguage, languageOptions } = useMemo(() => { let selectedLanguage!: Language; const languageOptions: Array = []; - Languages.forEach((language) => { + LANGUAGES.forEach((language) => { if (language.code === selectedLanguageCode) selectedLanguage = language; else languageOptions.push(language); }); diff --git a/web/src/redux/actions/settings.ts b/web/src/redux/actions/settings.ts index 7cd63f07b..667ab2cca 100644 --- a/web/src/redux/actions/settings.ts +++ b/web/src/redux/actions/settings.ts @@ -1,17 +1,17 @@ -import { Languages } from "@dzcode.io/models/dist/language"; +import { DEFAULT_LANGUAGE, LANGUAGES } from "@dzcode.io/models/dist/language"; import { LanguageCode } from "@dzcode.io/utils/dist/language"; import { captureException } from "@sentry/react"; export const changeLanguage = (languageCode: LanguageCode) => { let newPath = window.location.pathname; - const language = Languages.find(({ code }) => code === languageCode); + const language = LANGUAGES.find(({ code }) => code === languageCode); if (!language) { console.error("Invalid language code", languageCode); captureException(`Invalid language code ${language}`, { tags: { type: "GENERIC" } }); return; } - const urlLanguageRegEx = new RegExp(`^/(${Languages.map(({ code }) => code).join("|")})`); + const urlLanguageRegEx = new RegExp(`^/(${LANGUAGES.map(({ code }) => code).join("|")})`); const urlLanguageMatch = newPath.match(urlLanguageRegEx); if (urlLanguageMatch) { @@ -21,7 +21,7 @@ export const changeLanguage = (languageCode: LanguageCode) => { } // remove code from url if it's the default language - if (language.code === Languages[0].code) { + if (language.code === DEFAULT_LANGUAGE.code) { newPath = newPath.replace(`/${language.code}`, "") || "/"; } diff --git a/web/src/utils/website-language.ts b/web/src/utils/website-language.ts index 448709a8e..ad637c5e0 100644 --- a/web/src/utils/website-language.ts +++ b/web/src/utils/website-language.ts @@ -1,11 +1,12 @@ -import { Languages } from "@dzcode.io/models/dist/language"; +import { DEFAULT_LANGUAGE, LANGUAGES } from "@dzcode.io/models/dist/language"; import { LanguageCode } from "@dzcode.io/utils/dist/language"; let initialLanguageCode: LanguageCode | null = null; export function getInitialLanguageCode(): LanguageCode { if (!initialLanguageCode) { const language = - Languages.find(({ code }) => window.location.pathname.startsWith(`/${code}`)) || Languages[0]; + LANGUAGES.find(({ code }) => window.location.pathname.startsWith(`/${code}`)) || + DEFAULT_LANGUAGE; initialLanguageCode = language.code; } if (initialLanguageCode === "ar") { From e6a4702e71c9186e6558a2818bdf980420506d4a Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Wed, 1 Jan 2025 01:43:33 +0100 Subject: [PATCH 7/8] fix import --- web/lighthouserc.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lighthouserc.cjs b/web/lighthouserc.cjs index c86d8ee99..1e9edb6fc 100644 --- a/web/lighthouserc.cjs +++ b/web/lighthouserc.cjs @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-require-imports -const { Languages } = require("@dzcode.io/models/dist/language"); +const { LANGUAGES } = require("@dzcode.io/models/dist/language"); const baseUrl = process.env.LH_TEST_BASE_URL; const serverBaseUrl = process.env.LH_SERVER_BASE_URL; @@ -20,7 +20,7 @@ module.exports = { collect: { url: urls.reduce((acc, path) => { return acc.concat( - Languages.map(({ code }) => `${baseUrl}${code === "en" ? "" : `/${code}`}${path}`), + LANGUAGES.map(({ code }) => `${baseUrl}${code === "en" ? "" : `/${code}`}${path}`), ); }, []), }, From ded91cbbf15ba6b232a3539648a29b1048ca4c58 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Thu, 2 Jan 2025 10:11:33 +0100 Subject: [PATCH 8/8] localized search endpoint --- api/src/search/controller.ts | 2 +- api/src/search/service.ts | 30 ++++++++++++++++++++++++++---- api/src/search/types.ts | 3 ++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/api/src/search/controller.ts b/api/src/search/controller.ts index cdcd10f97..7d9bc587b 100644 --- a/api/src/search/controller.ts +++ b/api/src/search/controller.ts @@ -11,7 +11,7 @@ export class SearchController { @Get("/") public async search(@QueryParams() req: SearchQuery): Promise { - const searchResults = await this.searchService.search(req.query, req.limit); + const searchResults = await this.searchService.search(req.query, req.lang, req.limit); return { searchResults, }; diff --git a/api/src/search/service.ts b/api/src/search/service.ts index b2156b32e..dd7626039 100644 --- a/api/src/search/service.ts +++ b/api/src/search/service.ts @@ -5,6 +5,7 @@ import { ConfigService } from "src/config/service"; import { LoggerService } from "src/logger/service"; import { MeiliSearch } from "meilisearch"; import { Service } from "typedi"; +import { LanguageCode } from "@dzcode.io/utils/dist/language"; @Service() export class SearchService { @@ -25,20 +26,41 @@ export class SearchService { }); } - public search = async (q: string, limit?: number): Promise => { + public search = async (q: string, lang: LanguageCode, limit?: number): Promise => { + // TODO-ZM: only fetch Ids from search db, then query actually entities from their respective repositories this.logger.info({ message: `Searching for "${q}" in all indexes` }); const searchResults = await this.meilisearch.multiSearch({ queries: [ - { indexUid: "project", q, limit, attributesToRetrieve: ["id", "name"] }, + { indexUid: "project", q, limit, attributesToRetrieve: ["id", `name_${lang}`] }, { indexUid: "contribution", q, limit, - attributesToRetrieve: ["id", "title", "type", "activityCount", "url"], + attributesToRetrieve: ["id", `title_${lang}`, "type", "activityCount", "url"], + }, + { + indexUid: "contributor", + q, + limit, + attributesToRetrieve: ["id", `name_${lang}`, "avatarUrl"], }, - { indexUid: "contributor", q, limit, attributesToRetrieve: ["id", "name", "avatarUrl"] }, ], }); + + searchResults.results.forEach((result) => { + result.hits.forEach((hit) => { + if (hit[`name_${lang}`]) { + hit.name = hit[`name_${lang}`]; + delete hit[`name_${lang}`]; + } + + if (hit[`title_${lang}`]) { + hit.title = hit[`title_${lang}`]; + delete hit[`title_${lang}`]; + } + }); + }); + return searchResults as SearchResults; }; diff --git a/api/src/search/types.ts b/api/src/search/types.ts index e2b752e1f..e9556032b 100644 --- a/api/src/search/types.ts +++ b/api/src/search/types.ts @@ -4,8 +4,9 @@ import { GeneralResponse } from "src/app/types"; import { MultiSearchResponse } from "meilisearch"; import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { IsNotEmpty, IsPositive, IsString } from "class-validator"; +import { LanguageQuery } from "src/_utils/language"; -export class SearchQuery { +export class SearchQuery extends LanguageQuery { @IsString() @IsNotEmpty() query!: string;