@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 +43 -3
- package/src/components/members-grid.tsx +59 -0
- package/src/components/members.tsx +16 -0
- package/src/lib/members.ts +59 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrafirmagreg-team/crowdin",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
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