@terrafirmagreg-team/crowdin 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/crowdin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.1",
4
+ "type": "module",
5
+ "description": "Crowdin project members (translator avatars)",
6
+ "license": "LGPL-3.0-only",
7
+ "exports": {
8
+ "./lib/*": [
9
+ "./src/lib/*.ts",
10
+ "./src/lib/*/index.ts"
11
+ ],
12
+ "./components/*": [
13
+ "./src/components/*.tsx",
14
+ "./src/components/*/index.tsx",
15
+ "./src/components/*/index.ts"
16
+ ]
17
+ },
18
+ "files": [
19
+ "src"
20
+ ],
21
+ "dependencies": {
22
+ "@terrafirmagreg-team/data": "^1.2.5",
23
+ "@terrafirmagreg-team/kit": "^1.2.18",
24
+ "@terrafirmagreg-team/translate": "^1.2.9",
25
+ "@terrafirmagreg-team/ui": "^1.2.12",
26
+ "lucide-react": "^1.70.0",
27
+ "server-only": "^0.0.1"
28
+ },
29
+ "peerDependencies": {
30
+ "react": ">=19",
31
+ "react-dom": ">=19"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^26.6.6",
35
+ "@types/react": "^19.3.0",
36
+ "@types/react-dom": "^19.3.0"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/TerraFirmaGreg-Team/Web-Foundation.git",
41
+ "directory": "packages/crowdin"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
6
46
  }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import type { CrowdinMember } from '../lib/members';
4
+ import {
5
+ Avatar,
6
+ AvatarFallback,
7
+ AvatarImage,
8
+ } from '@terrafirmagreg-team/ui/components/ui/avatar';
9
+ import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
10
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
11
+ import { Users } from 'lucide-react';
12
+
13
+ export function MembersGrid({
14
+ url,
15
+ members,
16
+ }: {
17
+ url: string;
18
+ members: CrowdinMember[];
19
+ }) {
20
+ const t = useTranslations();
21
+ return (
22
+ <section className="not-prose mt-8 border-t pt-6">
23
+ <h2 className="mb-4 flex items-center gap-2 text-sm font-medium text-muted-foreground">
24
+ <Users className="size-4" />
25
+ <a
26
+ href={url}
27
+ target="_blank"
28
+ rel="noreferrer noopener"
29
+ className="hover:text-foreground hover:underline"
30
+ >
31
+ {t('Translators', { note: 'crowdin.translators' })}
32
+ </a>
33
+ </h2>
34
+ <ul className="flex flex-wrap gap-1.5">
35
+ {members.map((m) => (
36
+ <li key={m.username}>
37
+ <Hint label={m.fullName ?? m.username}>
38
+ <a
39
+ href={`https://crowdin.com/profile/${m.username}`}
40
+ target="_blank"
41
+ rel="noreferrer noopener"
42
+ >
43
+ <Avatar>
44
+ <AvatarImage
45
+ src={m.avatarUrl}
46
+ alt={m.fullName ?? m.username}
47
+ />
48
+ <AvatarFallback>
49
+ {m.username.slice(0, 1).toUpperCase()}
50
+ </AvatarFallback>
51
+ </Avatar>
52
+ </a>
53
+ </Hint>
54
+ </li>
55
+ ))}
56
+ </ul>
57
+ </section>
58
+ );
59
+ }
@@ -0,0 +1,16 @@
1
+ import 'server-only';
2
+ import { sectionConfig } from '@terrafirmagreg-team/kit/lib/config';
3
+ import type { SectionPageContext } from '@terrafirmagreg-team/kit/types/features';
4
+ import { crowdinMembers, crowdinProjectSlug } from '../lib/members';
5
+ import { MembersGrid } from './members-grid';
6
+
7
+ export async function CrowdinMembers({ ctx }: { ctx: SectionPageContext }) {
8
+ const url =
9
+ ctx.datasetId && ctx.page.slugs.length === 2
10
+ ? sectionConfig().datasetConfig[ctx.datasetId]?.crowdin
11
+ : undefined;
12
+ const slug = crowdinProjectSlug(url);
13
+ const members = slug ? await crowdinMembers(slug) : undefined;
14
+ if (!url || !members?.length) return null;
15
+ return <MembersGrid url={url} members={members} />;
16
+ }
@@ -0,0 +1,59 @@
1
+ import 'server-only';
2
+ import { createAsyncCache } from '@terrafirmagreg-team/data/lib/async-cache';
3
+
4
+ const CROWDIN_ORG = 'terrafirmagreg';
5
+ const API = `https://${CROWDIN_ORG}.api.crowdin.com/api/v2`;
6
+ const PAGE_SIZE = 500;
7
+
8
+ export type CrowdinMember = {
9
+ username: string;
10
+ fullName: string | null;
11
+ avatarUrl: string;
12
+ roles?: { name: string }[];
13
+ };
14
+
15
+ async function api<T>(path: string): Promise<T | undefined> {
16
+ const token = process.env.CROWDIN_API_TOKEN;
17
+ if (!token) return undefined;
18
+ const res = await fetch(`${API}${path}`, {
19
+ headers: { Authorization: `Bearer ${token}` },
20
+ next: { revalidate: 3600 },
21
+ } as RequestInit);
22
+ if (!res.ok) return undefined;
23
+ return (await res.json()) as T;
24
+ }
25
+
26
+ const projectId = createAsyncCache(async (identifier: string) => {
27
+ const res = await api<{
28
+ data: { data: { id: number; identifier: string } }[];
29
+ }>(`/projects?limit=${PAGE_SIZE}`);
30
+ return res?.data.find((p) => p.data.identifier === identifier)?.data.id;
31
+ });
32
+
33
+ export const crowdinMembers = createAsyncCache(
34
+ async (identifier: string): Promise<CrowdinMember[] | undefined> => {
35
+ const id = await projectId(identifier);
36
+ if (id === undefined) return undefined;
37
+ const members: CrowdinMember[] = [];
38
+ for (let offset = 0; ; offset += PAGE_SIZE) {
39
+ const res = await api<{ data: { data: CrowdinMember }[] }>(
40
+ `/projects/${id}/members?limit=${PAGE_SIZE}&offset=${offset}`,
41
+ );
42
+ const batch = res?.data ?? [];
43
+ members.push(...batch.map((m) => m.data));
44
+ if (batch.length < PAGE_SIZE) break;
45
+ }
46
+ return members;
47
+ },
48
+ (id) => id,
49
+ 3_600_000,
50
+ );
51
+
52
+ export function crowdinProjectSlug(url: string | undefined) {
53
+ if (!url) return undefined;
54
+ try {
55
+ return new URL(url).pathname.split('/').filter(Boolean).pop();
56
+ } catch {
57
+ return undefined;
58
+ }
59
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.