Skip to content

feat(xcloc): allow uploading updated xcloc files to update existing projects#31

Merged
sirily11 merged 4 commits intomainfrom
copilot/update-xcode-translation-files
Mar 3, 2026
Merged

feat(xcloc): allow uploading updated xcloc files to update existing projects#31
sirily11 merged 4 commits intomainfrom
copilot/update-xcode-translation-files

Conversation

Copy link
Contributor

Copilot AI commented Mar 3, 2026

Adds the ability to update existing Xcode localization projects by uploading a new .xcloc file — preserving existing translations for matching entries while adding new terms and removing stale ones. Mirrors the existing PO update pattern.

Core logic

  • XclocClient.updateFromXcloc(payload) — merges new xcloc archive into existing project, matching entries by composite key (resourceId + "\x00" + entryId). Syncs preserved translations back into the XLIFF doc for correct exports. Returns { added, removed, preserved, total } stats.

UI

  • XclocUpdateDialog — drop zone for .zip/.xcloc, validates bundle structure, shows diff preview badges before confirm
  • Generalized onUpdatePoonUpdate prop in TranslationEditor so the toolbar "Update" button works for both PO and xcloc formats

Tests

  • 4 unit tests covering merge logic, cross-resource preservation, export round-trip, and input validation
  • 2 e2e tests: diff preview with correct stats, and persistence after reload
  • Test asset zh-Hans-updated.xcloc.zip (3 entries removed, 2 added vs original)

i18n

  • Chinese translations added for all new UI strings

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • fonts.googleapis.com
    • Triggering command: /opt/hostedtoolcache/node/24.13.1/x64/bin/node /opt/hostedtoolcache/node/24.13.1/x64/bin/node /home/REDACTED/work/universal-translation/universal-translation/node_modules/next/dist/server/lib/start-server.js (dns block)
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/universal-translation/universal-translation/node_modules/.bin/next build (dns block)
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/universal-translation/universal-translation/node_modules/.bin/next build git l-tr�� get --global /home/REDACTED/.dotnet/tools/git user.name (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

update existing xcode translation implementation, allow upload updated translation files to update existing project. like new terms needed to be translated.

write e2e test to ensure the quality. run e2e before the commit


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
universal-translation Ready Ready Preview, Comment Mar 3, 2026 6:28am

Request Review

…rojects

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
…ions

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
Copilot AI changed the title [WIP] Update Xcode translation implementation to upload new files feat(xcloc): allow uploading updated xcloc files to update existing projects Mar 3, 2026
@sirily11 sirily11 marked this pull request as ready for review March 3, 2026 07:28
Copilot AI review requested due to automatic review settings March 3, 2026 07:28
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an “update existing project from uploaded file” flow for Xcode .xcloc projects, mirroring the existing PO update UX and enabling incremental localization updates while retaining matching translations.

Changes:

  • Implemented XclocClient.updateFromXcloc() to merge a newly uploaded xcloc archive into an existing project and return update stats.
  • Added XclocUpdateDialog and generalized the editor toolbar prop (onUpdatePoonUpdate) so the Update button works for both PO and xcloc.
  • Added unit/e2e tests plus an updated xcloc zip fixture, and updated i18n strings (en/zh).

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
lib/translation/xcloc/client.ts Adds xcloc merge/update logic and stats reporting.
lib/translation/xcloc/xcloc-update-dialog.tsx New UI dialog to upload/validate xcloc archives and preview update stats.
app/[locale]/dashboard/projects/[id]/editor-client.tsx Wires the Update button to open the right dialog for PO/xcloc and applies updates.
lib/translation/components/types.ts Renames editor prop to onUpdate to support multiple formats.
lib/translation/components/translation-editor.tsx Uses new onUpdate prop to render Update actions in toolbar/menu.
__tests__/translation/xcloc/client.test.ts Adds unit tests for xcloc merge/update behavior and export correctness.
e2e/xcloc-update.spec.ts Adds end-to-end coverage for xcloc update flow and reload behavior.
messages/en.po Adds new UI strings for xcloc update flow.
messages/zh.po Adds Chinese translations for new xcloc update strings.
test-assets/zh-Hans-updated.xcloc.zip New fixture used by unit/e2e tests for update scenarios.

Comment on lines +34 to +40
const dialogFileInput = page
.locator("input[accept='.zip,.xcloc']")
.first();
await dialogFileInput.setInputFiles(
path.resolve(__dirname, "../test-assets/zh-Hans-updated.xcloc.zip"),
);

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

This test locates the update dialog’s file input via input[accept='.zip,.xcloc'], which is brittle (there may be multiple file inputs on the page with the same accept string, including the initial upload input). Prefer targeting the dialog input via the dedicated test id (xcloc-update-file-input) to avoid intermittent mis-selection.

Suggested change
const dialogFileInput = page
.locator("input[accept='.zip,.xcloc']")
.first();
await dialogFileInput.setInputFiles(
path.resolve(__dirname, "../test-assets/zh-Hans-updated.xcloc.zip"),
);
const dialogFileInput = page.getByTestId("xcloc-update-file-input");
await dialogFileInput.setInputFiles(
path.resolve(__dirname, "../test-assets/zh-Hans-updated.xcloc.zip"),
);

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +128
test("should preserve translations after xcloc update and page reload", async ({
page,
}) => {
// 1. Navigate to new project page and upload original xcloc
await page.goto("/dashboard/projects/new");
await expect(page.getByTestId("tab-upload")).toBeVisible();
const fileInput = page.getByTestId("upload-file-input");
await fileInput.setInputFiles(
path.resolve(__dirname, "../test-assets/zh-Hans.xcloc.zip"),
);

// 2. Wait for editor
await expect(page).toHaveURL(/\/dashboard\/projects\/[a-f0-9-]+/, {
timeout: 15_000,
});
await expect(page.getByTestId("translation-editor")).toBeVisible({
timeout: 10_000,
});

// 3. Click Update and upload the updated xcloc
const updateButton = page.getByRole("button", { name: /update/i }).first();
await updateButton.click();

const dialogFileInput = page
.locator("input[accept='.zip,.xcloc']")
.first();
await dialogFileInput.setInputFiles(
path.resolve(__dirname, "../test-assets/zh-Hans-updated.xcloc.zip"),
);

// 4. Wait for diff preview and confirm
await expect(page.getByTestId("xcloc-update-diff-preview")).toBeVisible({
timeout: 5_000,
});
await page.getByTestId("xcloc-update-confirm").click();
await expect(
page.getByTestId("xcloc-update-diff-preview"),
).not.toBeVisible();

// 5. Editor should still be visible after update
await expect(page.getByTestId("translation-editor")).toBeVisible();

// 6. Wait for auto-save (debounce 5s + buffer)
await page.waitForTimeout(7_000);

// 7. Reload and verify the project still loads correctly
const currentUrl = page.url();
await page.goto(currentUrl);
await expect(page.getByTestId("translation-editor")).toBeVisible({
timeout: 10_000,
});
});
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The test name says it “should preserve translations … after … reload”, but it never sets or asserts any translations before/after the update/reload (it only checks that the editor loads). Either add assertions that a known translated entry remains translated after reload (or that preserved count > 0 and a specific target text is retained), or rename the test to reflect what it actually verifies.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +183
const readAndPreview = useCallback(
async (selectedFile: File) => {
setLoading(true);
setParseError(null);
try {
const processor = new DefaultUploadProcessor();
const result = await processor.process(selectedFile);
if (result.hasError) {
setParseError(result.errorMessage);
setPayload(null);
setDiffStats(null);
return;
}

if (result.data.kind !== "archive") {
setParseError(
t("The selected file is not a valid xcloc archive."),
);
setPayload(null);
setDiffStats(null);
return;
}

const stats = computeDiffStats(currentProject, result.data);
if (!stats) {
setParseError(
t("Could not parse the xcloc bundle. Ensure it contains a valid contents.json and XLIFF file."),
);
setPayload(null);
setDiffStats(null);
return;
}

setPayload(result.data);
setDiffStats(stats);
} catch {
setParseError(t("Failed to read the file. Please ensure it is a valid xcloc archive."));
} finally {
setLoading(false);
}
},
[currentProject, t],
);

const handleFileSelect = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const selected = e.target.files?.[0];
if (selected) {
setFile(selected);
readAndPreview(selected);
}
},
[readAndPreview],
);

const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
const dropped = e.dataTransfer.files[0];
if (dropped) {
setFile(dropped);
readAndPreview(dropped);
}
},
[readAndPreview],
);

const canConfirm = payload !== null && !parseError;

const handleConfirm = useCallback(() => {
if (!payload) return;
onConfirm(payload);
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

In readAndPreview, the previous successful payload/diffStats are kept while a new file is being parsed. Because canConfirm doesn’t consider loading, the Confirm button can remain enabled during parsing and could submit the stale payload. Clear payload/diffStats at the start of readAndPreview and disable confirm while loading is true.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +80
function computeDiffStats(
currentProject: TranslationProject,
payload: UploadPayload,
): DiffStats | null {
if (payload.kind !== "archive") return null;

const files = payload.tree.files.filter(
(f) => !f.path.includes("__MACOSX"),
);

const contentsFile = files.find((f) => f.path.endsWith("contents.json"));
if (!contentsFile) return null;

const contentsResult = parseContentsJson(contentsFile.content);
if (contentsResult.hasError) return null;

const xliffFile = files.find(
(f) =>
f.path.includes("Localized Contents/") && f.path.endsWith(".xliff"),
);
if (!xliffFile) return null;

const xliffXml = new TextDecoder().decode(xliffFile.content);
const newXliffDoc = parseXliff(xliffXml);

// Build set of old keys
const oldKeys = new Set<string>();
for (const resource of currentProject.resources) {
for (const entry of resource.entries) {
oldKeys.add(`${resource.id}\x00${entry.id}`);
}
}

// Build set of new keys
const newKeys = new Set<string>();
for (const file of newXliffDoc.files) {
for (const tu of file.transUnits) {
newKeys.add(`${file.original}\x00${tu.id}`);
}
}

const preserved = [...newKeys].filter((k) => oldKeys.has(k)).length;
const added = [...newKeys].filter((k) => !oldKeys.has(k)).length;
const removed = [...oldKeys].filter((k) => !newKeys.has(k)).length;

return { added, removed, preserved, total: newKeys.size };
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The dialog’s preserved stat counts overlapping entry keys regardless of whether the existing entry actually has a translation, but XclocClient.updateFromXcloc() reports preserved only when a non-empty targetText is carried over. This can make the preview badges (and the “no translations preserved” warning) disagree with the actual update result/toast. Consider aligning the preview’s preserved definition with the update logic (e.g., count only entries with non-empty current targetText) or rename the badge to clarify it’s “matching entries”.

Copilot uses AI. Check for mistakes.
...this.project,
resources: newResources,
sourceLanguage: contentsResult.data.developmentRegion,
targetLanguages: [contentsResult.data.targetLocale],
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

updateFromXcloc() updates this.contentsJson from the new bundle, but it does not update this.project.metadata (which is initially derived from contentsJson in load()). After an update, project.metadata.project/toolInfo can become stale relative to the persisted contentsJson. Update the project metadata alongside sourceLanguage/targetLanguages to keep state consistent.

Suggested change
targetLanguages: [contentsResult.data.targetLocale],
targetLanguages: [contentsResult.data.targetLocale],
metadata: {
...this.project.metadata,
project: {
...(this.project.metadata && this.project.metadata.project),
toolInfo:
contentsResult.data.project && contentsResult.data.project.toolInfo
? contentsResult.data.project.toolInfo
: this.project.metadata &&
this.project.metadata.project &&
this.project.metadata.project.toolInfo,
},
},

Copilot uses AI. Check for mistakes.
@sirily11 sirily11 merged commit fd994c7 into main Mar 3, 2026
11 checks passed
@sirily11 sirily11 deleted the copilot/update-xcode-translation-files branch March 3, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants