Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
eff3e15
feat: Phase 1 scope freeze — AI Visual Commerce QA
ofcskn May 17, 2026
4fa8fb8
docs: add MVP executable roadmap and initialize web tsconfig build info
ofcskn May 17, 2026
2fe8d38
feat: Phase 2 domain layer — ProductWorkflowStatus, ExportProfile, mo…
ofcskn May 18, 2026
86c6bc3
feat: Phase 2 UI — approval gate wired to ProductWorkflowStatus
ofcskn May 18, 2026
898f8e4
feat: implement AiDiagnosisPanel to replace inline AI analysis and pr…
ofcskn May 18, 2026
14685de
feat: implement SourceImageReadiness domain model, UI component, and …
ofcskn May 18, 2026
cfc17ec
feat: add model source tracking to domain aggregates and implement Mo…
ofcskn May 18, 2026
534bef7
feat: implement hotspot quality validation logic and introduce Hotspo…
ofcskn May 18, 2026
c56fded
feat: implement HotspotQuality validation logic and update UI specs
ofcskn May 18, 2026
62665fb
feat: implement hotspot approval system and quality validation logic …
ofcskn May 18, 2026
f14c13a
feat: integrate HotspotEditorPanel with validation logic in ProductDe…
ofcskn May 18, 2026
896526d
docs: add reference and how-to documentation for Phase 6 hotspot qual…
ofcskn May 18, 2026
b23f3a6
docs: add Hotspot Quality reference documentation covering validation…
ofcskn May 18, 2026
ee91716
feat: implement product URL import workflow with new data structures,…
ofcskn May 18, 2026
82b7d28
feat: implement product URL import workflow and provenance tracking v…
ofcskn May 18, 2026
0c21434
feat: implement product import domain logic, database schema, and exp…
ofcskn May 18, 2026
c2ccfb6
feat: implement product URL import workflow with database schema upda…
ofcskn May 18, 2026
75b18b2
feat: implement product URL import pipeline with new domain statuses,…
ofcskn May 18, 2026
7da114e
feat: implement product URL import workflow with status tracking, dat…
ofcskn May 18, 2026
9fb5faf
feat: implement product URL import workflow with status tracking and …
ofcskn May 18, 2026
ae0b54f
feat: add workflow statuses, import provenance columns, and roadmap f…
ofcskn May 18, 2026
a458408
feat: add product import workflow status and provenance fields, and d…
ofcskn May 18, 2026
fcf1c23
docs: add Phase 0 roadmap for Product URL Import and AI autofill inte…
ofcskn May 18, 2026
78d2b3f
feat: filter source assets by supported Gemini MIME types before proc…
ofcskn May 18, 2026
65c9916
Merge pull request #8 from ofcskn/feature/phase1-scope-freeze
ofcskn May 18, 2026
102c3da
refactor: fix indentation in MediaAsset instantiation and remove max-…
ofcskn May 18, 2026
3e11ff1
feat(core): extend domain model with APUS image intelligence and mult…
ofcskn May 18, 2026
be442e7
feat(ai): add Gemini APUS intelligence services — image classifier, m…
ofcskn May 18, 2026
d9cc849
feat(api): add APUS import pipeline — adapters, orchestrator, and ext…
ofcskn May 18, 2026
4a59b75
feat(web): add MultiProductClusterSelector component and wire cluster…
ofcskn May 18, 2026
1a9f463
feat(supabase): annotate import_data column with APUS v1 field inventory
ofcskn May 18, 2026
166c0fb
docs: add APUS pipeline explanation and URL import how-to in English …
ofcskn May 18, 2026
6780325
Merge pull request #9 from ofcskn/feature/apus-product-import
ofcskn May 18, 2026
55a0da7
fix debug
May 18, 2026
d97c3ba
feat(ai): GLB pipeline v2 — semantic scene graph architecture
ofcskn May 18, 2026
83c345d
feat: add automated git staging and commit script for individual file…
ofcskn May 18, 2026
68c6ef9
Merge pull request #10 from ofcskn/feature/glb-pipeline-v2-scene-graph
ofcskn May 18, 2026
408f069
feat: add i18n-engineer skill documentation for automated translation…
ofcskn May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions .claude/skills/i18n-engineer/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
name: i18n-engineer
description: Automatically internationalize React components in the minimalblock app for Turkish (tr) and English (en). Detects static text, generates keys, adds translations to both locale files, and replaces hardcoded strings with t() calls. No static text allowed — only i18n.
---

# I18n Engineer

## Use when
- A component or page contains any hardcoded string literal rendered in JSX
- A new page or component is added and needs translating
- A user requests "add i18n", "internationalize this", or "no static text"
- `placeholder`, `aria-label`, `title`, `alt` attributes have hardcoded strings

## Never leave behind
- String literals inside JSX (e.g. `<p>Loading...</p>`)
- Template literals with UI text (e.g. `` `Hello ${name}` ``)
- `placeholder="..."`, `aria-label="..."`, `alt="..."` with literal values
- Button/label/option text hardcoded in JSX

---

## Project i18n stack

| Item | Value |
|------|-------|
| Library | `react-i18next` + `i18next` |
| Config | `apps/web/src/i18n/index.ts` |
| English | `apps/web/src/i18n/locales/en.ts` |
| Turkish | `apps/web/src/i18n/locales/tr.ts` |
| Hook | `useTranslation()` from `react-i18next` |
| Call | `t('namespace.key')` or `t('namespace.key', { var })` |
| Default lang | Turkish (`tr`) |
| Fallback lang | English (`en`) |

Both locale files export a **TypeScript `as const` object** — no JSON files, no separate namespace files.

---

## Namespace map

Pick the namespace based on where the component lives:

| Namespace | Use for |
|-----------|---------|
| `nav` | Sidebar, top-bar navigation labels |
| `common` | Shared actions: Cancel, Save, Delete, Back, Refresh |
| `auth` | Login, signup, password, email, OAuth strings |
| `profile` | User profile menu items |
| `category` | Product category labels |
| `gallery` | Gallery page, QA queue, toolbar, empty states |
| `upload` | Upload/conversion form and flow |
| `dashboard` | Analytics dashboard, charts, orders widget |
| `orders` | Orders list page, table columns, statuses |
| `product` | Product detail, QA review, modals (embed, reject, trendyol) |

Add a new top-level namespace only when the content clearly does not fit any existing one.

---

## Key naming rules

- `camelCase` for all key names
- Nest with objects when a screen section groups ≥ 3 keys (e.g. `deleteModal.title`)
- Use `_one` / `_other` suffixes for plurals (i18next plural convention)
- Use `{{variableName}}` for interpolated values
- Prefix with the namespace in the `t()` call: `t('gallery.loadMore')`

---

## Workflow — step by step

### 1. Read the target file
Read the full component. Identify every hardcoded string:
- Text nodes: `<span>Foo</span>` → `Foo`
- Attribute strings: `placeholder="Enter name"`, `aria-label="Close"`, `alt="Product image"`
- Conditional text: `error ? 'Failed' : 'Success'`
- Template literal UI text: `` `${count} items` ``

### 2. Determine namespaces and keys
Map each string to a `namespace.key`. Reuse existing keys from the locale files when they match exactly (check `en.ts` first).

### 3. Write translations for both locales

**English (`en.ts`)**: use the original English text (or a clean version of it).

**Turkish (`tr.ts`)**: translate accurately. Guidelines:
- "Cancel" → "İptal", "Save" → "Kaydet", "Delete" → "Sil", "Back" → "Geri"
- Use formal/polite tone (siz-based where needed)
- Keep brand names (Trendyol, Gemini, GLB) unchanged
- Preserve `{{variable}}` placeholders verbatim

Edit **both** locale files in the same response — never add a key to one without the other.

### 4. Replace static text in the component

Add the import if missing:
```tsx
import { useTranslation } from 'react-i18next';
```

Add the hook inside the component if missing:
```tsx
const { t } = useTranslation();
```

Replace each hardcoded string:
```tsx
// Before
<button>Save changes</button>
// After
<button>{t('common.save')}</button>

// Before — interpolation
<p>{count} products synced</p>
// After
<p>{t('nav.productsSynced', { count })}</p>

// Before — attribute
<input placeholder="Enter product name" />
// After
<input placeholder={t('upload.productNamePlaceholder')} />
```

### 5. Verify
- No string literals remain in JSX or attributes (except technical values like CSS class names, IDs, event names, URLs)
- Both `en.ts` and `tr.ts` have the new keys at identical paths
- The component still type-checks (`npx nx typecheck web`)
- Run `npx nx dev web` if possible to do a quick visual check

---

## Locale file editing rules

Both files end with `} as const;`. Insert new keys inside the correct namespace object, maintaining alphabetical order within the group where possible.

**Example — adding `gallery.filterLabel`:**

In `en.ts`:
```ts
gallery: {
// ... existing keys ...
filterLabel: 'Filter products',
// ...
},
```

In `tr.ts`:
```ts
gallery: {
// ... existing keys ...
filterLabel: 'Ürünleri filtrele',
// ...
},
```

---

## Edge cases

| Situation | Handling |
|-----------|----------|
| String is purely a CSS class / HTML attribute with no user-visible meaning | Leave as-is |
| String is a URL or route path | Leave as-is |
| String is a log message or `console.error` | Leave as-is |
| String is an enum value / status code passed to logic | Leave as-is; only translate the display label |
| Component receives a string prop from parent | Translate at the call site, not inside the component |
| Dynamic key (e.g. `t(\`category.${slug}\`)`) | Use only when the key set is finite and all variants exist in locales |
12 changes: 12 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "api",
"version": "0.0.1",
"private": true,
"dependencies": {
"@minimalblock/ai": "workspace:*",
"@minimalblock/core": "workspace:*",
"@minimalblock/data": "workspace:*",
"@minimalblock/trendyol": "workspace:*",
"@supabase/supabase-js": "^2.105.4"
}
}
31 changes: 31 additions & 0 deletions apps/api/src/lib/import/adapters/adapter-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { IPageScraperAdapter } from '@minimalblock/core';
import { MockAdapter } from './mock.adapter.js';
import { AmazonAdapter } from './amazon.adapter.js';
import { IkeaAdapter } from './ikea.adapter.js';
import { GenericHtmlAdapter } from './generic.adapter.js';

function normalizeDomain(url: URL): string {
return url.hostname.toLowerCase().replace(/^www\./, '');
}

const SUPPORTED_DOMAINS = new Set(['amazon.com', 'etsy.com', 'ikea.com', 'trendyol.com']);

export class ScraperAdapterRegistry {
private readonly adapters: IPageScraperAdapter[];

constructor() {
this.adapters = [
new MockAdapter(),
new AmazonAdapter(),
new IkeaAdapter(),
];
}

resolve(url: URL): IPageScraperAdapter {
const found = this.adapters.find((adapter) => adapter.canHandle(url));
if (found) return found;
const domain = normalizeDomain(url);
const level = SUPPORTED_DOMAINS.has(domain) ? 'supported' : 'best_effort';
return new GenericHtmlAdapter(level);
}
}
15 changes: 15 additions & 0 deletions apps/api/src/lib/import/adapters/amazon.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ScrapedPageData } from '@minimalblock/core';
import { GenericHtmlAdapter } from './generic.adapter.js';

export class AmazonAdapter extends GenericHtmlAdapter {
override readonly supportLevel = 'supported' as const;

override canHandle(url: URL): boolean {
return /amazon\.(com|co\.uk|de|fr|it|es|co\.jp|ca|com\.au)$/.test(url.hostname.toLowerCase().replace(/^www\./, ''));
}

override async scrape(url: URL): Promise<ScrapedPageData> {
const base = await super.scrape(url);
return base;
}
}
Loading
Loading