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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
383 changes: 383 additions & 0 deletions .cursor/skills/i18n-memsource/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
---
name: i18n-memsource
description: >-
Automates the Memsource/Phrase i18n translation workflow for nmstate-console-plugin.
Use when the user asks to upload translations, download translations, check translation
status, memsource upload, memsource download, i18n upload, i18n download, send for
translation, or get translations.
---

# nmstate-console-plugin i18n Memsource Workflow

Manages upload/download of translations to Phrase (Memsource) for this repo.

Uses the **existing** local `i18n-scripts/` + `i18next-parser` setup.
Does **not** use `ocp-plugin-i18n-scripts` or `i18next-cli`.

For a peer-oriented walkthrough, see [USAGE.md](./USAGE.md).

## State

Read and update `.cursor/skills/i18n-memsource/state.json` after each upload.

## Plugin config

| Field | Value |
|-------|-------|
| Namespace / locale file | `plugin__nmstate-console-plugin` |
| Memsource template ID | `zBOwr4BxYwEq7xlJ37c1F3` |
| Project title | `[OCP $VERSION] UI Localization nmstate-console-plugin - Sprint $SPRINT/Branch $BRANCH` |
| Languages | `ja`, `zh-cn`, `ko`, `fr`, `es` |
| Locale dirs on disk | `en`, `es`, `fr`, `ja`, `ko`, **`zh`** (not `zh-cn`) |
| PO filename pattern | `po-files/<lang>/plugin__nmstate-console-plugin.po` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use one PO filename contract.

Line [32] documents plugin__nmstate-console-plugin.po. Line [382] says that PO basenames must not be hard-coded because this repository uses a public__ prefix. These instructions conflict.

Use po-files/<lang>/*.po in the configuration, or document the verified generated basename in both locations.

Proposed documentation fix
-| PO filename pattern | `po-files/<lang>/plugin__nmstate-console-plugin.po` |
+| PO filename pattern | `po-files/<lang>/*.po` |

Also applies to: 382-382

🧰 Tools
🪛 SkillSpector (2.5.1)

[warning] 149: [RA2] Session Persistence: Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Remediation: Remove any persistence mechanisms (cron jobs, startup scripts, state files). Skills should not maintain state across sessions without explicit user consent.

(Rogue Agent (RA2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/skills/i18n-memsource/SKILL.md at line 32, Align the PO filename
guidance in the i18n-memsource instructions: replace the hard-coded
plugin__nmstate-console-plugin.po pattern with po-files/<lang>/*.po, and update
the related guidance around the basename rule so both locations consistently
avoid hard-coded PO basenames unless the verified generated basename is
documented in both places.


## Prerequisites

### Memsource CLI

```bash
MEMSOURCE_BIN=$(python3 -c "import shutil; print(shutil.which('memsource') or '')")
if [ -z "$MEMSOURCE_BIN" ]; then
MEMSOURCE_BIN=$(find "$HOME/Library/Python" -name memsource -type f 2>/dev/null | head -1)
fi
export PATH="$(dirname "$MEMSOURCE_BIN"):$PATH"
```

### Authentication (credentials stay with the user)

**Do not** read `~/.memsourcerc`, Memsource passwords, or long-lived tokens into
the agent context. Phrase is a paid external service — treat credentials like
any other secret.

Preferred flow:

1. Ask the user to authenticate in **their own terminal** and confirm
`memsource auth whoami` works (and that `MEMSOURCE_TOKEN` is exported in the
shell they will use for `npm run memsource-*`).
2. The agent may run extract/export/validation without credentials.
3. For upload/download/status, either:
- the user runs the `memsource-*` commands themselves after the agent prepares
artifacts, or
- the user has already exported a **short-lived** `MEMSOURCE_TOKEN` in the
shared shell (never paste the password into chat).

If a shared shell already has `MEMSOURCE_TOKEN`, verify with:

```bash
memsource auth whoami
```

If auth fails, stop and ask the user to refresh the token outside the agent.
Also ensure `jq` is installed (`brew install jq`) — upload scripts need it.

---

## Critical nuances

### 1. zh-cn vs zh filesystem mismatch

`i18n-scripts/languages.sh` uses `zh-cn`, but locales live under `locales/zh/`.
Without a symlink, `i18n-to-po` cannot merge existing Chinese translations and
uploads empty msgstr for Chinese.

**Before any `export-pos` during upload**, create the symlink and register
unconditional cleanup (also on failure / cancel):

```bash
ln -sfn zh locales/zh-cn
trap 'rm -f locales/zh-cn; rm -rf po-files locales/tmp' EXIT
```
Comment on lines +83 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp=$(mktemp -d)
mkdir -p "$tmp/locales"
(
  ln -s zh "$tmp/locales/zh-cn"
  trap 'rm -f "$tmp/locales/zh-cn"' EXIT
)
test ! -e "$tmp/locales/zh-cn"
echo "A separate shell removes the symlink before the next workflow step."

Repository: openshift/nmstate-console-plugin

Length of output: 238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked skill file exists:\n'
ls -l .cursor/skills/i18n-memsource/SKILL.md

printf '\nRelevant upload section (lines 60-115):\n'
sed -n '60,115p' .cursor/skills/i18n-memsource/SKILL.md | cat -n -v

printf '\nCleanup/export-pos references around lines 180-215:\n'
sed -n '180,215p' .cursor/skills/i18n-memsource/SKILL.md | cat -n -v

printf '\nAll export-pos / cleanup mentions in file:\n'
rg -n 'export-pos|rm -f|trap|locales/zh-cn|cleanup|po-files|locales/tmp' .cursor/skills/i18n-memsource/SKILL.md

Repository: openshift/nmstate-console-plugin

Length of output: 4996


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Upload workflow section (lines 170-330):\n'
sed -n '170,330p' .cursor/skills/i18n-memsource/SKILL.md | nl -ba

printf '\nCommand block formatting around upload steps:\n'
rg -n '^<{3}bash|^```(bash)?|### Step 5|### Step 6|### Step 7|### Step 8|export-pos|cleanup|rm -f|rm -rf' .cursor/skills/i18n-memsource/SKILL.md

Repository: openshift/nmstate-console-plugin

Length of output: 251


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Upload workflow section (lines 170-330):\n'
sed -n '170,330p' .cursor/skills/i18n-memsource/SKILL.md | awk '{printf "%6d\t%s\n", NR+169, $0}'

printf '\nCommand block references in upload workflow:\n'
rg -n '^<\|bash|<\|bash|^```(bash)?|### Step 5|### Step 6|### Step 7|### Step 8|export-pos|cleanup|rm -f|rm -rf' .cursor/skills/i18n-memsource/SKILL.md

Repository: openshift/nmstate-console-plugin

Length of output: 7319


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('.cursor/skills/i18n-memsource/SKILL.md')
s = p.read_text()
steps = [i for i, line in enumerate(s.splitlines(), 1) if "### Step " in line]
print("Step headings:", steps)
for start in steps:
    line = s.splitlines()[start-1]
    raw = line.replace('###', '').replace(':', '').strip()
    print(f"{start}: {raw}")

print("\nFenced bash block starts:")
for i, line in enumerate(s.splitlines(), 1):
    if line.startswith("```bash"):
        print(i)

print("\nTrap command starts:")
for i, line in enumerate(s.splitlines(), 1):
    if line.startswith("trap "):
        print(f"{i}: {line}")
PY

Repository: openshift/nmstate-console-plugin

Length of output: 1000


Ensure the upload cleanup trap survives until Step 8.

The EXIT trap is installed in Step 5, but export-pos runs in a separate fenced command block before memsource-upload. If each block is a separate shell, the trap exits and deletes locales/zh-cn before the upload re-runs export-pos, so Chinese translations can be dropped from the generated POs. Keep the install and cleanup in one wrapper script that also runs validation and upload, or use an explicit cleanup step after the upload succeeds or fails.

🧰 Tools
🪛 SkillSpector (2.5.1)

[warning] 149: [RA2] Session Persistence: Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Remediation: Remove any persistence mechanisms (cron jobs, startup scripts, state files). Skills should not maintain state across sessions without explicit user consent.

(Rogue Agent (RA2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/skills/i18n-memsource/SKILL.md around lines 83 - 89, The upload
workflow around the Step 5 symlink and EXIT trap must keep cleanup active
through the Step 8 export-pos and memsource-upload operations. Consolidate
symlink creation, validation, export-pos, and upload into one wrapper shell, or
add explicit cleanup that runs after upload success or failure; ensure
locales/zh-cn remains available until export-pos has completed.


Download already maps `zh-cn` → `zh`.

### 2. PO generation preserves existing translations

`i18n-to-po.js`:

1. Start from English keys in `locales/en/`
2. Clear values (empty placeholders)
3. Merge existing values from `locales/<lang>/`
4. Convert to PO via `i18next-conv`

Never skip `export-pos` or hand-build English-only POs.

### 3. English placeholders must be empty in uploaded POs

`i18next-parser` + `useKeysAsDefaultValue: true` can leave English text in
secondary locale JSON. Old `i18n-to-po` copies that into `msgstr`, which Phrase
may treat as already translated.

**Desired PO state before upload:**

| Locale JSON value | msgstr in PO |
|-------------------|--------------|
| Real non-English translation | Keep it (carry forward) |
| Empty `""` | Empty (needs translation) |
| English placeholder (== English source) | **Empty** (needs translation) |

`export-pos.sh` runs `i18n-scripts/clear-english-msgstr.js` after generating POs
to clear `msgstr` when it equals `msgid`. Because `memsource-upload.sh` re-runs
`export-pos`, that clear also applies to the files that get uploaded.

This clear step is **redundant** if the repo migrates to
[`ocp-plugin-i18n-scripts`](https://github.com/avivtur/ocp-plugin-i18n-scripts),
which filters English placeholders during PO generation.

### 4. Download clean-git check is wrong

`memsource-download.sh` checks `public/locales` / `packages/**/locales`, but this
repo uses root `locales/`. Before download, verify:

```bash
git status --short --untracked-files -- locales/
```

---

## Action 1: Upload Translations

Trigger: "upload translations", "memsource upload", "i18n upload", "send for translation"

### Checklist

```text
Upload Progress:
- [ ] Step 1: Load state
- [ ] Step 2: Get VERSION from user, auto-increment SPRINT
- [ ] Step 3: Confirm user-provided Memsource auth (no credential access)
- [ ] Step 4: Extract translation keys
- [ ] Step 5: Create zh-cn symlink, generate PO files, clear English msgstr
- [ ] Step 6: Validate PO files
- [ ] Step 7: Show summary and get approval
- [ ] Step 8: Upload to Memsource (user shell / short-lived token only)
- [ ] Step 9: Cleanup and update state
```

### Step 1: Load state

Read `.cursor/skills/i18n-memsource/state.json`.

### Step 2: Get VERSION and SPRINT

- Always ask the user for VERSION. Do not assume.
- Auto-increment SPRINT from state (previous + 1).
- Show: "Uploading VERSION X, Sprint Y (branch: Z)"

```bash
git branch --show-current
```

### Step 3: Confirm authentication

Do **not** source `~/.memsourcerc` or request the password in chat. Ask the user
to authenticate in their terminal, then verify `memsource auth whoami` in the
shell that will run upload (or have the user run Step 8 themselves).

### Step 4: Extract translation keys

```bash
npm run i18n
# → i18next … -c i18next-parser.config.js && node ./i18n-scripts/set-english-defaults.js
```

Then show:

```bash
git status --short -- locales/
git diff --stat -- locales/
```

Require approval if the locale diff looks wrong.

### Step 5: zh-cn symlink + export POs + clear English msgstr

```bash
ln -sfn zh locales/zh-cn
trap 'rm -f locales/zh-cn; rm -rf po-files locales/tmp' EXIT
rm -rf po-files
npm run export-pos
# export-pos ends with: node ./i18n-scripts/clear-english-msgstr.js
```

Keep the symlink until after Step 8 (`memsource-upload` re-runs `export-pos`,
which also re-runs the clear step). The `trap` ensures cleanup even if a later
step fails.

If validating a hand-run export that skipped the script hook:

```bash
node ./i18n-scripts/clear-english-msgstr.js
```

### Step 6: Validate PO files

After the clear step, English placeholders should be empty. Report:

- **translated** = non-empty msgstr and msgstr ≠ msgid (real carry-forward)
- **needs translation** = empty msgstr (includes former English placeholders)
- **english_leaks remaining** = msgstr == msgid (should be 0 after clear)

```bash
for lang in ja zh-cn ko fr es; do
echo "=== $lang ==="
python3 -c "
import re, glob, sys
files = glob.glob(f'po-files/{sys.argv[1]}/*.po')
content = ''.join(open(f).read() for f in files)
entries = re.findall(r'msgid \"((?:\\\\.|[^\"])*)\"\s*msgstr \"((?:\\\\.|[^\"])*)\"', content)
entries = [(a,b) for a,b in entries if a]
translated = sum(1 for a,b in entries if b and b != a)
needs = sum(1 for a,b in entries if not b)
leaks = sum(1 for a,b in entries if b and b == a)
print(f' total={len(entries)} translated={translated} needs_translation={needs} english_leaks_remaining={leaks}')
" "$lang"
done
```

Warn if `english_leaks_remaining > 0`.

### Step 7: Show summary and get approval

Present plugin, version, sprint, branch, validation results, and project title.
Ask for explicit approval before upload.

### Step 8: Upload to Memsource

```bash
npm run memsource-upload -- -v "$VERSION" -s "$SPRINT"
```

Capture `PROJECT_ID` (`.uid`) from `memsource project create` output.

### Step 9: Cleanup and update state

```bash
rm -f locales/zh-cn
rm -rf po-files locales/tmp
```

Update `state.json` with `version`, `sprint`, `lastProjectId`, `memsourceProjectUrl`, and history.

Draft notification:

```text
Subject: [OCP VERSION] Translation Upload - nmstate-console-plugin Sprint SPRINT

Hi Localization Team,

New translation strings have been uploaded for nmstate-console-plugin
(OCP VERSION, Sprint SPRINT).

Memsource project: https://cloud.memsource.com/web/project2/show/PROJECT_ID

Languages: ja, zh-cn, ko, fr, es
Total keys: N

Please review and translate at your convenience. Let us know when translations
are ready for download.

Thanks
```

---

## Action 2: Download Translations

Trigger: "download translations", "memsource download", "i18n download", "get translations"

### Checklist

```text
Download Progress:
- [ ] Step 1: Load state / confirm PROJECT_ID
- [ ] Step 2: Confirm user-provided Memsource auth
- [ ] Step 3: Check translation status
- [ ] Step 4: Ensure locales/ is clean
- [ ] Step 5: Download translations
- [ ] Step 6: Show diff summary
- [ ] Step 7: Create PR (optional)
```

### Step 1: Confirm PROJECT_ID

Show `lastProjectId` from state; ask to confirm or override.

### Step 2: Confirm authentication

Same as upload Step 3 — user-owned credentials only; no `~/.memsourcerc` in agent context.

### Step 3: Status

```bash
for lang in ja zh-cn ko fr es; do
memsource job list \
--project-id "$PROJECT_ID" \
--target-lang "$lang" \
-f json \
-c uid,status,targetLang
done
```

Warn if not all completed; ask before proceeding.

### Step 4: Clean locales

```bash
git status --short --untracked-files -- locales/
# Must be clean — commit or stash first
```

### Step 5: Download

Create/switch to the PR branch **before** download (the script auto-commits):

```bash
git checkout -b "chore/i18n-update-sprint-${SPRINT}"
npm run memsource-download -- -p "$PROJECT_ID"
```
Comment on lines +332 to +337

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Define SPRINT before creating the download branch.

The download workflow confirms PROJECT_ID, but it never loads or asks for SPRINT. Line [335] therefore creates chore/i18n-update-sprint-, or fails when the shell uses set -u. git checkout -b also fails when the branch already exists.

Load SPRINT from state or request it from the user. Then switch to the existing branch or create it when absent.

Proposed workflow fix
+SPRINT=$(jq -er '.sprint' .cursor/skills/i18n-memsource/state.json)
+BRANCH="chore/i18n-update-sprint-${SPRINT}"
+
+if git show-ref --verify --quiet "refs/heads/${BRANCH}"; then
+  git switch "$BRANCH"
+else
+  git switch -c "$BRANCH"
+fi
+
-git checkout -b "chore/i18n-update-sprint-${SPRINT}"
 npm run memsource-download -- -p "$PROJECT_ID"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Create/switch to the PR branch **before** download (the script auto-commits):
```bash
git checkout -b "chore/i18n-update-sprint-${SPRINT}"
npm run memsource-download -- -p "$PROJECT_ID"
```
Create/switch to the PR branch **before** download (the script auto-commits):
🧰 Tools
🪛 SkillSpector (2.5.1)

[warning] 149: [RA2] Session Persistence: Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Remediation: Remove any persistence mechanisms (cron jobs, startup scripts, state files). Skills should not maintain state across sessions without explicit user consent.

(Rogue Agent (RA2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/skills/i18n-memsource/SKILL.md around lines 332 - 337, Update the
download workflow around the branch creation and memsource-download commands to
load SPRINT from state or prompt the user before constructing the branch name.
Check whether chore/i18n-update-sprint-${SPRINT} already exists, switching to it
when present and creating it only when absent, then run the existing download
command.


This downloads POs, converts with `po-to-i18n` (`zh-cn` → `zh`), and auto-commits.

### Step 6–7: Diff + optional PR

```bash
git diff HEAD~1 --stat -- locales/

git push -u origin HEAD
Comment thread
coderabbitai[bot] marked this conversation as resolved.
gh pr create --title "chore(i18n): update translations for Sprint ${SPRINT}" --body "$(cat <<EOF
## Summary
- Downloaded translations from Memsource project ${PROJECT_ID}
- Languages: ja, zh-cn, ko, fr, es

## Memsource Project
https://cloud.memsource.com/web/project2/show/${PROJECT_ID}

## Test plan
- [ ] Verify locale files are valid JSON
- [ ] Spot-check translations in the UI

Resolves: None
EOF
)"
```

---

## Action 3: Status only

1. Load `lastProjectId` (confirm/override)
2. Authenticate
3. Run job list for each language
4. If all completed, suggest download

---

## Important reminders

- Always symlink `locales/zh-cn` → `zh` before upload `export-pos`; remove after upload
- Never skip `export-pos` (it preserves real translations and clears English placeholders)
- After export, `needs_translation` should cover empty msgstr; `english_leaks_remaining` should be 0
- Clean root `locales/` before download
- Update `state.json` after successful upload
- Glob PO files (`*.po`); do not hard-code PO basenames (this repo uses `public__` prefix)
- If this repo migrates to `ocp-plugin-i18n-scripts`, remove `clear-english-msgstr.js` from `export-pos.sh` (redundant)
Loading