# Migrating an existing project

Move an existing translation dataset into Comvi without losing data or breaking production: key cap, pilot, structured import, reconciliation, cutover.

Canonical: https://comvi.io/docs/platform/recipes/migrating-large-project

Four things go wrong in a migration: partial imports, encoding damage, lost history and
comments, and drift between the old source and the new one during the cutover. Sequence the
work so all four stay visible.

## Check the key cap

Comvi caps translation keys per organization and enforces the cap on every key created. The
and the limit. Count your source keys first; if the number is above the plan's limit, cut
scope or upgrade before you import. Going over the key, member or
project limit puts the organization into read-only, where every write fails with
`403 READ_ONLY_MODE`, so leave headroom. Keys, projects, members and MT credits are all
capped per plan, and languages are not; read the [current limits](https://comvi.io/pricing).

## Inventory

| Metric | Why it matters |
|---|---|
| **Key count** | Checked against the key limit before anything else |
| **Keys per namespace or file** | Drives the namespace layout |
| **File formats** | Comvi imports JSON files and ZIP archives of JSON only. i18next-shaped JSON imports, and non-ICU plural syntax is converted; anything else needs converting first |
| **Placeholder format** | `{{name}}` and `%s` are converted or need converting to ICU |
| **Scripts in use** | Arabic, Hebrew and CJK content is where encoding damage shows |

Decide the cutover model too. A **hard cutover** freezes the old system and Comvi takes
over; it needs a freeze window. **Writing to both systems** for a while is lower risk and
costs a reconciliation pass at the end. A dataset that fits one import batch usually takes a
hard cutover of a day or two.

## Pilot

Before touching the full dataset, take the smallest namespace or the top 200 keys, one
source language plus one target, through the whole [import](#import). Check keys, values,
statuses, ICU placeholders, special characters and line breaks against the source. Budget a
day.

## Set up the target project

1. **Create the project** with your source language ([Projects](/docs/platform/projects)).
   It cannot be changed after creation.

2. **Add the target languages** ([Languages](/docs/platform/languages)). Codes are canonical
   BCP-47: `pt-BR`, `zh-Hans`, `en-US`.

3. **Create the namespaces** matching the current file layout. Do not restructure while
   migrating; a namespace with keys cannot be deleted.

4. **Import the glossary**, so the glossary check has something to say from the first
   review. See [Glossaries](/docs/platform/glossaries).

5. **Import the translation memory** as TMX if the old system exports it, so fuzzy matches
   are live afterwards. See [Translation memory](/docs/platform/translation-memory).

## Import

1. **Export from the old system** into one JSON file per language per namespace, and
   normalize: `{{name}}` to `{name}`, `%s` to `{arg1}`, stray braces escaped as `'{'`,
   UTF-8 only (`file -I *.json`).

2. **Lay the archive out so detection works.** The wizard reads the namespace and locale
   from each path:

   ```text
   import.zip/
   ├── common/
   │   ├── en.json
   │   ├── de.json
   │   └── …
   └── checkout/
       ├── en.json
       └── …
   ```

   The mapping table on the import screen lets you set namespace and locale per file and
   create a missing namespace before committing. Limits per batch: 50 files, 10 MB each,
   500 entries per ZIP, 100,000 keys, and a session expires after one hour.

3. **Read the validation report.** Uploading validates without writing: per file, the
   detected format, conflicts with existing content, and whether it parsed. The summary
   counts files, total keys and unique keys, and flags a key-limit overrun.

4. **Resolve conflicts.** A conflict is a key and locale that already holds a different
   value. Choose **Keep existing** or **Use imported** per conflict or in bulk, and set the
   **Unresolved Conflict Policy** for the rest. A first pass into an empty project has
   nothing to conflict with.

5. **Commit, then check what landed.** Every non-empty value lands as `Translated` with the
   importing user as reviewer; empty values land as `Not Translated`. To keep a review step
   for untrusted content, select the imported rows on the Translations page and use
   **Status → Mark as Needs Review**.

6. **Import the remaining languages** three to five at a time, so each job stays short and
   an error stays attributable.

See [Import & export](/docs/platform/import-export) for the full wizard reference.

## Reconcile

[`GET /v1/projects/:id/locales/stats`](/docs/api/languages) returns per-locale totals and
percentages, and the [Languages](/docs/platform/languages) page shows the same numbers.

- **Total keys** should equal the inventory. A shortfall means a file failed to parse or was
left unmapped.
- **Translated count per locale** should match what the old system called approved.
- **Placeholder integrity.** For every source `{foo}`, each target should contain `{foo}`.
Comvi does not verify this on save or import.
- **Encoding.** Search for `?` runs and sequences like `Ã©` and `â€™`.

Fix the gaps with a second, targeted import.

> **Re-importing after fixing the export**
>
> **Use imported** per conflict, or a policy of **Prefer imported values**, overwrites work
> done in Comvi since the previous import. Decide the resolution before the second run.

## Cut over

1. **Point staging at the new endpoint.** Enable **Settings → Content Deploy**, copy the
   endpoint URL, and pass it to the fetch loader as
   [`cdnUrl`](/docs/i18n/plugins/fetch-loader#options) in staging.

2. **Run the app's QA suite against staging.** Watch plurals, right-to-left languages, and
   long-string languages such as German and Russian.

3. **Swap production.** Change the production `cdnUrl`, deploy, and keep the old system
   readable but not writable as fallback.

4. **Watch for missing keys for a day.** A key that did not arrive shows up as a missing-key
   warning from the i18n library at runtime.

5. **Decommission.** Revoke the old system's credentials, archive the old export files, and
   send the translation team the editor URL and
   [Your first project](/docs/platform/your-first-project).

## History and comments

Comvi imports values, not the old system's change history or comments, and neither can be
back-dated. Either accept the break, with Comvi's history starting at the import, or export
the old history as CSV and keep it next to the repository. Move any comment a translator
needs into the key's **Internal notes (not sent to MT)**, or into
**AI translation context (sent to MT)** if a provider should see it too.

## Time budget

| Phase | Duration |
|---|---|
| Inventory and pilot | 1 to 2 days |
| Normalization and validation | 1 to 2 days |
| Full import and reconciliation | 1 day |
| Staging QA | 1 to 2 days |
| Cutover and monitoring | 1 day |

For roughly 3,000 keys across 10 languages, five to eight days in total. The longest parts
are outside Comvi: converting placeholder syntax and running your own QA suite. Freeze
writes on both systems for the cutover window, and check whether the old system's
"translated" means per-language approval before deciding what the import should write.

## Next steps

- [Import & export](/docs/platform/import-export): Wizard, formats, conflict resolution.
- [Translation memory](/docs/platform/translation-memory): Load TMX early so fuzzy matches are live on day one.
- [Glossaries](/docs/platform/glossaries): Import terminology before translations.
- [Onboarding agencies](/docs/platform/recipes/onboarding-agencies): Bring external translators in after cutover.
