# Web App Internationalization Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add vue-i18n v8 multilingual support to the Vue 2 web app with 6 languages (EN, RU, TR, BE, KA, UK), a flag-based language switcher, and an independent print language selector.

**Architecture:** Install vue-i18n v8 as a Vue plugin with eagerly-loaded JSON locale files. All hardcoded English strings are replaced with `$t()` / `$tc()` calls. A `LanguageSelector` component in the header switches the UI language. A separate print language selector on the Share view controls the language of printed QR shard pages, passing a `locale` prop through `ShardInfo` → `ShardQrCode`.

**Tech Stack:** Vue 2, vue-i18n v8, TypeScript, JSON locale files

**Spec:** `docs/superpowers/specs/2026-03-18-web-app-i18n-design.md`

---

## File Structure

### New Files

| File | Responsibility |
|------|---------------|
| `src/i18n.ts` | Create VueI18n instance with locale detection, pluralization rules, and eagerly-imported messages |
| `src/components/LanguageSelector.vue` | Flag-based dropdown to switch `$i18n.locale` |
| `src/locales/en.json` | English translations (~80 keys) |
| `src/locales/ru.json` | Russian translations |
| `src/locales/tr.json` | Turkish translations |
| `src/locales/be.json` | Belarusian translations |
| `src/locales/ka.json` | Georgian translations |
| `src/locales/uk.json` | Ukrainian translations |

### Modified Files

| File | Changes |
|------|---------|
| `src/main.ts` | Import and install vue-i18n, pass i18n instance to root Vue |
| `src/App.vue` | Add LanguageSelector, replace logo text with `$t()`, add `html lang` watcher |
| `src/views/Share.vue` | Replace ~20 strings, add `printLocale` data + print language dropdown |
| `src/views/Combine.vue` | Replace ~14 strings including error messages |
| `src/views/Print.vue` | Replace ~11 strings including error messages |
| `src/views/Info.vue` | Replace 3 nav button labels |
| `src/components/GeneralInfo.vue` | Replace all headings and paragraphs (~11 keys) |
| `src/components/ShardQrCode.vue` | Accept `locale` prop, use `$t(key, locale, params)` |
| `src/components/ShardInfo.vue` | Inject i18n into detached Vue instance, forward `locale` prop |
| `src/components/ForkMe.vue` | Replace 1 string |

---

## Chunk 1: Foundation

### Task 1: Install vue-i18n and create i18n module

**Files:**
- Modify: `package.json`
- Create: `src/i18n.ts`
- Create: `src/locales/en.json`

- [ ] **Step 1: Install vue-i18n v8**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn add vue-i18n@^8'
```

Expected: vue-i18n 8.x added to package.json dependencies.

- [ ] **Step 2: Create the English locale file**

Create `src/locales/en.json` with all translatable strings extracted from the web app components. Keys use camelCase with feature prefixes matching Flutter ARB naming.

```json
{
  "appTitle": "Banana Split",

  "tabCreate": "Create",
  "tabRestore": "Restore",
  "tabPrint": "Print",

  "createTitle": "Create a secret split",
  "createNameLabel": "1. Name of your split",
  "createNameHint": "Ex: 'My Bitcoin seed phrase'",
  "createSecretLabel": "2. Secret",
  "createSecretHint": "Your secret goes here",
  "createSecretTooLong": "Secret exceeds 1024 characters",
  "createCharCounter": "{remaining} / 1024 characters remaining",
  "createShardsLabel": "3. Shards",
  "createShardsRequire": "Will require any",
  "createShardsOutOf": "shards out of",
  "createShardsReconstruct": "to reconstruct",
  "createPassphraseLabel": "4. Recovery passphrase",
  "createPassphraseHint": "Enter passphrase (min 8 characters)",
  "createPassphraseTooShort": "Passphrase must be at least 8 characters",
  "createPassphraseCustom": "Use custom passphrase",
  "createGenerateButton": "Generate QR codes!",
  "createBackButton": "Back to editing data",
  "createPrintButton": "Print us!",
  "createPrintLanguage": "Print language",

  "combineTitle": "Combine shards",
  "combineFor": "for",
  "combinePassphraseLabel": "1. Secret Phrase",
  "combinePassphraseHint": "type your passphrase",
  "combineSecretLabel": "2. Secret",
  "combineReconstructButton": "Reconstruct Secret",

  "errorShardSeen": "Shard already seen",
  "errorTitleMismatch": "title mismatch!",
  "errorNonceMismatch": "nonce mismatch!",
  "errorRequiredMismatch": "requiredShards mismatch",

  "printTitle": "Print shards",
  "printShardsLabel": "Shards",
  "printShardsQuestion": "How many shards have you generated?",
  "printImportButton": "Import QR codes!",
  "printInvalidShards": "Please enter a valid number of shards between 3 and 255.",

  "shardNeedMore": "You need {count} more QR code like this to reconstruct the secret | You need {count} more QR codes like this to reconstruct the secret",
  "shardRecoveryPassphrase": "Recovery passphrase is",
  "shardDownloadPrompt": "Please go to {link} to download the reconstruction webapp, if you don't have one already.",
  "shardVersionInfo": "This has been generated by BananaSplit version {version}",

  "forkMe": "Fork me on GitHub",

  "infoWhatIsThis": "What is this?",
  "infoWhatIsThisBody": "Banana Split is a self-contained HTML page, which makes your paper backups more resilient and secure using {link}. You can encrypt and split particularly sensitive information so that it's not physically stored in one place — your master password, seed phrase, etc.",
  "infoShamirLink": "Shamir's secret sharing scheme",
  "infoHowDoesItWork": "How does it work?",
  "infoHowBody1": "After you type in your secret into Banana Split, it will be encrypted with an autogenerated passphrase and split into N QR-codes, ready to be printed out. You'll need N/2+1 of those printouts to reconstruct the secret, and then the passphrase to decrypt it.",
  "infoHowBody2": "Banana Split tries to protect your secret from the attack vectors like \"attacker is able to intercept everything you're sending to your printer\", and that's why you'll have to write down the passphrase on your printouts by hand.",
  "infoHowBody3": "Of course you should seal your printouts in non-transparent envelopes and distribute among different locations as soon as you're done writing your seed phrase — Banana Split is only more secure than a single paper with your secret as long as the secrets can't be lost/stolen altogether.",
  "infoHowBody4": "All operations must be performed in Offline mode. Recovery can be done on any device with a webcam — just show your QR codes to the webcam and follow the notifications on screen in the process.",
  "infoHowToUse": "How to use it?",
  "infoStep1": "Save this HTML page into your local Documents folder",
  "infoStep2": "Turn off Wi-Fi, Bluetooth and any other connectivity on your device",
  "infoStep3": "Open the downloaded HTML page and follow the instructions",
  "infoMinimalRisk": "This way the risk of compromise will be minimal."
}
```

- [ ] **Step 3: Create the i18n module**

Create `src/i18n.ts`:

```typescript
import Vue from "vue";
import VueI18n from "vue-i18n";

import en from "./locales/en.json";
import ru from "./locales/ru.json";
import tr from "./locales/tr.json";
import be from "./locales/be.json";
import ka from "./locales/ka.json";
import uk from "./locales/uk.json";

Vue.use(VueI18n);

const SUPPORTED_LOCALES = ["en", "ru", "tr", "be", "ka", "uk"];

function detectLocale(): string {
  const browserLang = navigator.language?.split("-")[0];
  if (browserLang && SUPPORTED_LOCALES.includes(browserLang)) {
    return browserLang;
  }
  return "en";
}

// Slavic plural rule: one | few | many
// e.g., 1 фрагмент | 2 фрагмента | 5 фрагментов
function slavicPlural(choice: number): number {
  const abs = Math.abs(choice) % 100;
  const lastDigit = abs % 10;
  if (lastDigit === 1 && abs !== 11) return 0;
  if (lastDigit >= 2 && lastDigit <= 4 && (abs < 12 || abs > 14)) return 1;
  return 2;
}

const i18n = new VueI18n({
  locale: detectLocale(),
  fallbackLocale: "en",
  messages: { en, ru, tr, be, ka, uk },
  pluralizationRules: {
    ru: slavicPlural,
    uk: slavicPlural,
    be: slavicPlural
  }
});

export default i18n;
export { SUPPORTED_LOCALES };
```

- [ ] **Step 4: Wire i18n into main.ts**

Modify `src/main.ts` to import and use the i18n instance:

```typescript
import Vue from "vue";

// @ts-ignore
import VueQriously from "vue-qriously";
Vue.use(VueQriously);

// @ts-ignore
import QrcodeStream from "vue-qrcode-reader";
Vue.use(QrcodeStream);

import App from "./App.vue";
import router from "./router";
import i18n from "./i18n";

Vue.config.productionTip = false;

Vue.prototype.$eventHub = new Vue();

new Vue({
  router,
  i18n,
  render: function(h) {
    return h(App);
  }
}).$mount("#app");
```

- [ ] **Step 5: Verify the app compiles**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn build 2>&1 | tail -5'
```

Expected: Build succeeds. (Locale files ru/tr/be/ka/uk don't exist yet — create them as empty `{}` temporarily, or stub them with the same content as en.json for now.)

**Note:** Before building, create stub locale files for the 5 non-English locales so the imports in `i18n.ts` resolve:

```bash
for lang in ru tr be ka uk; do cp src/locales/en.json src/locales/$lang.json; done
```

These will be replaced with real translations in Task 7.

- [ ] **Step 6: Run existing tests to confirm nothing is broken**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn test:unit 2>&1 | tail -5'
```

Expected: All 11 tests pass. Tests use the crypto module directly and don't depend on i18n.

- [ ] **Step 7: Run lint**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn lint 2>&1 | tail -5'
```

Expected: No lint errors.

- [ ] **Step 8: Commit**

```bash
git add src/i18n.ts src/locales/ src/main.ts package.json yarn.lock
git commit -m "feat(i18n): install vue-i18n v8 and create i18n module with locale detection"
```

---

### Task 2: Create LanguageSelector component and add to App.vue

**Files:**
- Create: `src/components/LanguageSelector.vue`
- Modify: `src/App.vue`

- [ ] **Step 1: Create the LanguageSelector component**

Create `src/components/LanguageSelector.vue`:

```vue
<template>
  <div class="language-selector">
    <button class="language-trigger" @click="open = !open" :title="currentLabel">
      {{ currentFlag }}
    </button>
    <ul v-if="open" class="language-dropdown">
      <li
        v-for="locale in locales"
        :key="locale.code"
        class="language-option"
        :class="{ active: locale.code === $i18n.locale }"
        @click="selectLocale(locale.code)"
      >
        <span class="language-flag">{{ locale.flag }}</span>
        <span class="language-name">{{ locale.name }}</span>
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import Vue from "vue";

const LOCALES = [
  { code: "en", flag: "🇬🇧", name: "English" },
  { code: "ru", flag: "🇷🇺", name: "Русский" },
  { code: "tr", flag: "🇹🇷", name: "Türkçe" },
  { code: "be", flag: "🇧🇾", name: "Беларуская" },
  { code: "ka", flag: "🇬🇪", name: "ქართული" },
  { code: "uk", flag: "🇺🇦", name: "Українська" }
];

export default Vue.extend({
  name: "LanguageSelector",
  data() {
    return {
      open: false,
      locales: LOCALES
    };
  },
  computed: {
    currentFlag(): string {
      const current = LOCALES.find(l => l.code === this.$i18n.locale);
      return current ? current.flag : "🇬🇧";
    },
    currentLabel(): string {
      const current = LOCALES.find(l => l.code === this.$i18n.locale);
      return current ? current.name : "English";
    }
  },
  mounted() {
    document.addEventListener("click", this.handleClickOutside);
  },
  beforeDestroy() {
    document.removeEventListener("click", this.handleClickOutside);
  },
  methods: {
    selectLocale(code: string) {
      this.$i18n.locale = code;
      this.open = false;
    },
    handleClickOutside(event: Event) {
      const el = this.$el as HTMLElement;
      if (!el.contains(event.target as Node)) {
        this.open = false;
      }
    }
  }
});
</script>

<style>
.language-selector {
  position: relative;
  display: inline-block;
}
.language-trigger {
  font-size: 2.4rem;
  background: transparent;
  border: none;
  padding: 0.5rem;
  cursor: pointer;
  line-height: 1;
}
.language-dropdown {
  position: absolute;
  right: 0;
  top: 100%;
  background: var(--c_bg-card);
  border: 1px solid var(--c_border-main);
  border-radius: 0.5rem;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  list-style: none;
  margin: 0.25rem 0 0;
  padding: 0.25rem 0;
  z-index: 1000;
  min-width: 180px;
}
.language-option {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 1rem;
  cursor: pointer;
  font-size: 1.6rem;
  white-space: nowrap;
}
.language-option:hover {
  background: var(--c_bg-app);
}
.language-option.active {
  font-weight: 600;
}
.language-flag {
  font-size: 2rem;
}
@media print {
  .language-selector {
    display: none;
  }
}
</style>
```

- [ ] **Step 2: Add LanguageSelector to App.vue and add html lang watcher**

Modify `src/App.vue`:

In the template, add LanguageSelector to the nav and replace "Banana Split" with `$t()`:

```html
<nav>
  <div class="nav-top">
    <router-link id="logo" to="/">
      <span class="logo-text">
        {{ $t('appTitle') }}
      </span>
      <span class="logo-icon">
        🍌
      </span>
    </router-link>
    <LanguageSelector />
  </div>
  <Alert />
</nav>
```

In the script, import LanguageSelector and add the watcher:

```typescript
import Alert from "./components/Alert.vue";
import ForkMe from "./components/ForkMe.vue";
import LanguageSelector from "./components/LanguageSelector.vue";

import Vue from "vue";

export default Vue.extend({
  name: "App",
  components: { Alert, ForkMe, LanguageSelector },
  computed: {
    gitRevision: function() {
      return process.env.GIT_REVISION;
    }
  },
  watch: {
    "$i18n.locale"(newLocale: string) {
      document.documentElement.lang = newLocale;
    }
  },
  created() {
    document.documentElement.lang = this.$i18n.locale;
  }
});
```

Add CSS for the `.nav-top` layout:

```css
.nav-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}
```

- [ ] **Step 3: Verify the app builds and the language selector renders**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn build 2>&1 | tail -5'
```

- [ ] **Step 4: Run lint and tests**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn lint 2>&1 | tail -5 && yarn test:unit 2>&1 | tail -5'
```

- [ ] **Step 5: Commit**

```bash
git add src/components/LanguageSelector.vue src/App.vue
git commit -m "feat(i18n): add LanguageSelector component and html lang watcher"
```

---

## Chunk 2: Internationalize All Views

### Task 3: Internationalize Share.vue (including print language selector)

**Files:**
- Modify: `src/views/Share.vue`

- [ ] **Step 1: Replace all hardcoded strings with `$t()` calls**

Replace the Share.vue template. Every hardcoded English string becomes a `$t()` call. Add `printLocale` data and a print language dropdown next to the print button.

Template changes (key substitutions):

| Original | Replacement |
|----------|------------|
| `Create a secret split` | `{{ $t('createTitle') }}` |
| `1. Name of your split` | `{{ $t('createNameLabel') }}` |
| `placeholder="Ex: 'My Bitcoin seed phrase'"` | `:placeholder="$t('createNameHint')"` |
| `2. Secret` | `{{ $t('createSecretLabel') }}` |
| `placeholder="Your secret goes here"` | `:placeholder="$t('createSecretHint')"` |
| `{{ 1024 - secret.length }} / 1024 characters remaining` | `{{ $t('createCharCounter', { remaining: 1024 - secret.length }) }}` |
| `3. Shards` | `{{ $t('createShardsLabel') }}` |
| `Will require any` | `{{ $t('createShardsRequire') }}` |
| `shards out of` | `{{ $t('createShardsOutOf') }}` |
| `to reconstruct` | `{{ $t('createShardsReconstruct') }}` |
| `4. Recovery passphrase` | `{{ $t('createPassphraseLabel') }}` |
| `placeholder="Enter passphrase (min 8 characters)"` | `:placeholder="$t('createPassphraseHint')"` |
| `Passphrase must be at least 8 characters` | `{{ $t('createPassphraseTooShort') }}` |
| `Use custom passphrase` | `{{ $t('createPassphraseCustom') }}` |
| `Generate QR codes!` | `{{ $t('createGenerateButton') }}` |
| `Back to editing data` | `{{ $t('createBackButton') }}` |
| `Print us!` | `{{ $t('createPrintButton') }}` |

Add `printLocale` to the data:

```typescript
data(): ShareData {
  return {
    // ... existing fields ...
    printLocale: ""
  };
},
created: function() {
  this.regenPassphrase();
  this.printLocale = this.$i18n.locale;
},
```

Add a print language selector next to the print button in the encryption mode section. Use a simple `<select>` with flag options:

```html
<div class="flex align-center" style="gap: 8px; margin-bottom: 8px;">
  <button id="printBtn" class="button-card" @click="print">
    {{ $t('createPrintButton') }}
  </button>
  <select v-model="printLocale" class="print-locale-select">
    <option value="en">🇬🇧</option>
    <option value="ru">🇷🇺</option>
    <option value="tr">🇹🇷</option>
    <option value="be">🇧🇾</option>
    <option value="ka">🇬🇪</option>
    <option value="uk">🇺🇦</option>
  </select>
</div>
```

Pass `printLocale` as `locale` prop to `shard-info`:

```html
<shard-info
  v-for="shard in shards"
  :key="shard"
  :shard="shard"
  :required-shards="requiredShards"
  :title="title"
  :locale="printLocale"
/>
```

Add CSS for the print locale select:

```css
.print-locale-select {
  width: auto;
  padding: 0.5rem;
  font-size: 2rem;
  border: 1px solid var(--c_border-main);
  border-radius: 0.5rem;
  background: var(--c_bg-card);
  cursor: pointer;
}
```

- [ ] **Step 2: Verify build compiles**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn build 2>&1 | tail -5'
```

- [ ] **Step 3: Run lint and tests**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn lint 2>&1 | tail -5 && yarn test:unit 2>&1 | tail -5'
```

- [ ] **Step 4: Commit**

```bash
git add src/views/Share.vue
git commit -m "feat(i18n): internationalize Share view with print language selector"
```

---

### Task 4: Internationalize Combine.vue

**Files:**
- Modify: `src/views/Combine.vue`

- [ ] **Step 1: Replace all hardcoded strings**

| Original | Replacement |
|----------|------------|
| `Combine shards` | `{{ $t('combineTitle') }}` |
| `for` | `{{ $t('combineFor') }}` |
| `1. Secret Phrase` | `{{ $t('combinePassphraseLabel') }}` |
| `placeholder="type your passphrase"` | `:placeholder="$t('combinePassphraseHint')"` |
| `2. Secret` | `{{ $t('combineSecretLabel') }}` |
| `Reconstruct Secret` | `{{ $t('combineReconstructButton') }}` |
| `"Shard already seen"` (in `$eventHub.$emit`) | `this.$t('errorShardSeen')` |
| `"title mismatch!"` | `this.$t('errorTitleMismatch')` |
| `"nonce mismatch!"` | `this.$t('errorNonceMismatch')` |
| `"requiredShards mismatch"` | `this.$t('errorRequiredMismatch')` |

- [ ] **Step 2: Run lint and tests**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn lint 2>&1 | tail -5 && yarn test:unit 2>&1 | tail -5'
```

- [ ] **Step 3: Commit**

```bash
git add src/views/Combine.vue
git commit -m "feat(i18n): internationalize Combine view"
```

---

### Task 5: Internationalize Print.vue

**Files:**
- Modify: `src/views/Print.vue`

- [ ] **Step 1: Replace all hardcoded strings**

| Original | Replacement |
|----------|------------|
| `Print shards` | `{{ $t('printTitle') }}` |
| `Shards` | `{{ $t('printShardsLabel') }}` |
| `How many shards have you generated?` | `{{ $t('printShardsQuestion') }}` |
| `Import QR codes!` | `{{ $t('printImportButton') }}` |
| `for` | `{{ $t('combineFor') }}` (reuse same key) |
| `Print us!` | `{{ $t('createPrintButton') }}` (reuse same key) |
| `"title mismatch!"` (in `$eventHub.$emit`) | `this.$t('errorTitleMismatch')` |
| `"nonce mismatch!"` | `this.$t('errorNonceMismatch')` |
| `"Please enter a valid number..."` | `this.$t('printInvalidShards')` |

- [ ] **Step 2: Run lint and tests**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn lint 2>&1 | tail -5 && yarn test:unit 2>&1 | tail -5'
```

- [ ] **Step 3: Commit**

```bash
git add src/views/Print.vue
git commit -m "feat(i18n): internationalize Print view"
```

---

### Task 6: Internationalize Info.vue, GeneralInfo.vue, ForkMe.vue, ShardQrCode.vue, ShardInfo.vue

**Files:**
- Modify: `src/views/Info.vue`
- Modify: `src/components/GeneralInfo.vue`
- Modify: `src/components/ForkMe.vue`
- Modify: `src/components/ShardQrCode.vue`
- Modify: `src/components/ShardInfo.vue`

- [ ] **Step 1: Internationalize Info.vue**

Replace the 3 nav button labels:

```html
<router-link id="shareNav" class="button-nav" to="/share">
  {{ $t('tabCreate') }}
</router-link>
<router-link id="combineNav" class="button-nav" to="/combine">
  {{ $t('tabRestore') }}
</router-link>
<router-link id="printNav" class="button-nav" to="/print">
  {{ $t('tabPrint') }}
</router-link>
```

- [ ] **Step 2: Internationalize GeneralInfo.vue**

Replace all headings and paragraphs with `$t()` calls. For the "What is this?" paragraph that contains an `<a>` link to Shamir's Secret Sharing, use vue-i18n's `<i18n>` component for safe HTML interpolation:

```html
<template>
  <div class="general-info">
    <h2>{{ $t('infoWhatIsThis') }}</h2>
    <i18n path="infoWhatIsThisBody" tag="p">
      <template #link>
        <a href="https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing" target="_blank">
          {{ $t('infoShamirLink') }}
        </a>
      </template>
    </i18n>
    <h2>{{ $t('infoHowDoesItWork') }}</h2>
    <p>{{ $t('infoHowBody1') }}</p>
    <p>{{ $t('infoHowBody2') }}</p>
    <p>{{ $t('infoHowBody3') }}</p>
    <p>{{ $t('infoHowBody4') }}</p>
    <h2>{{ $t('infoHowToUse') }}</h2>
    <ol>
      <li>{{ $t('infoStep1') }}</li>
      <li>{{ $t('infoStep2') }}</li>
      <li>{{ $t('infoStep3') }}</li>
    </ol>
    <p>{{ $t('infoMinimalRisk') }}</p>
  </div>
</template>
```

Note the `infoWhatIsThisBody` translation string uses `{link}` as a placeholder:
```
"Banana Split is a self-contained HTML page, which makes your paper backups more resilient and secure using {link}. You can encrypt and split particularly sensitive information so that it's not physically stored in one place — your master password, seed phrase, etc."
```

- [ ] **Step 3: Internationalize ForkMe.vue**

Replace `Fork me on GitHub`:

```html
<a :href="url">🥄&nbsp;{{ $t('forkMe') }}</a>
```

- [ ] **Step 4: Internationalize ShardQrCode.vue**

Add `locale` prop. Use `$t(key, locale, { params })` for all text so it follows the print locale:

```vue
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
  name: "ShardQrCode",
  props: {
    title: { type: String, required: true },
    shard: { type: String, required: true },
    requiredShards: { type: Number, required: true },
    locale: { type: String, default: "" }
  },
  computed: {
    effectiveLocale(): string {
      return this.locale || this.$i18n.locale;
    },
    needMoreText(): string {
      const count = this.requiredShards - 1;
      return this.$tc("shardNeedMore", count, this.effectiveLocale, { count });
    },
    recoveryText(): string {
      return this.$t("shardRecoveryPassphrase", this.effectiveLocale) as string;
    },
    downloadText(): string {
      return this.$t("shardDownloadPrompt", this.effectiveLocale, {
        link: "nfcarchiver.com/banana"
      }) as string;
    },
    versionText(): string {
      return this.$t("shardVersionInfo", this.effectiveLocale, {
        version: this.gitRevision
      }) as string;
    },
    gitRevision(): string {
      return process.env.GIT_REVISION || "";
    }
  }
});
</script>
```

Template uses computed properties:

```html
<h3>{{ needMoreText }}</h3>
<!-- ... -->
<div class="recovery-title">{{ recoveryText }}&nbsp;</div>
<!-- ... -->
<p class="version">
  {{ downloadText }}<br />
  {{ versionText }}
</p>
```

Note: The `<a>` link to nfcarchiver.com in the print output. Since this is a printed page (not interactive), we can embed the URL as plain text in the translation. The link is decorative in print anyway.

- [ ] **Step 5: Fix ShardInfo.vue — inject i18n into detached Vue instance**

This is critical. ShardInfo creates a detached Vue instance that won't inherit i18n. Add `locale` prop and inject i18n:

```vue
<script lang="ts">
import Vue, { VNode } from "vue";
import ShardQrCode from "./ShardQrCode.vue";
import i18n from "../i18n";

export default Vue.extend({
  name: "ShardInfo",
  components: { ShardQrCode },
  props: {
    title: { type: String, required: true },
    shard: { type: String, required: true },
    requiredShards: { type: Number, required: true },
    locale: { type: String, default: "" }
  },
  beforeDestroy: function() {
    this.vm.$el.remove();
    this.vm.$destroy();
  },
  render: function(): VNode {
    const element = document.createElement("div");
    const print = document.getElementById("print");
    if (print) {
      print.appendChild(element);
    } else {
      // eslint-disable-next-line no-console
      console.warn("Failed to find `print` element");
    }
    const passedProps = this.$props;
    this.vm = new Vue({
      el: element,
      i18n,
      render: function(h) {
        return h(ShardQrCode, { props: passedProps });
      }
    });
    return this.vm.$vnode;
  }
});
</script>
```

- [ ] **Step 6: Run lint and build**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn lint 2>&1 | tail -5 && yarn build 2>&1 | tail -5'
```

- [ ] **Step 7: Run tests**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn test:unit 2>&1 | tail -5'
```

- [ ] **Step 8: Commit**

```bash
git add src/views/Info.vue src/components/GeneralInfo.vue src/components/ForkMe.vue src/components/ShardQrCode.vue src/components/ShardInfo.vue
git commit -m "feat(i18n): internationalize Info, GeneralInfo, ForkMe, ShardQrCode, and ShardInfo"
```

---

## Chunk 3: Translation Files

### Task 7: Create all translation locale files

**Files:**
- Create/Replace: `src/locales/ru.json`
- Create/Replace: `src/locales/tr.json`
- Create/Replace: `src/locales/be.json`
- Create/Replace: `src/locales/ka.json`
- Create/Replace: `src/locales/uk.json`

Port translations from the Flutter ARB files where keys overlap. For web-app-specific keys (GeneralInfo content, print-specific text, error messages), write new translations consistent with the Flutter app's tone and terminology.

- [ ] **Step 1: Create `src/locales/ru.json`**

Port from `banana_split_flutter/lib/l10n/app_ru.arb`. Map Flutter keys to web app keys. For web-only keys (GeneralInfo content), translate fresh. The Russian locale file should have the same key structure as `en.json`.

Key mapping from Flutter ARB → web JSON:
- `tabCreate` → `tabCreate`
- `tabRestore` → `tabRestore`
- `errorDuplicateShard` → `errorShardSeen`
- `errorTitleMismatch` → `errorTitleMismatch`
- `errorNonceMismatch` → `errorNonceMismatch`
- `errorRequiredMismatch` → `errorRequiredMismatch`
- `aboutDescription` → use for `infoWhatIsThisBody` (adapt wording)
- `pdfPassphrasePlaceholder` → `shardRecoveryPassphrase` (extract prefix)

For pluralized strings (e.g., `shardNeedMore`), Slavic locales need 3 forms separated by `|`:
```json
"shardNeedMore": "Вам нужен ещё {count} QR-код для восстановления секрета | Вам нужно ещё {count} QR-кода для восстановления секрета | Вам нужно ещё {count} QR-кодов для восстановления секрета"
```

- [ ] **Step 2: Create `src/locales/tr.json`**

Port from `banana_split_flutter/lib/l10n/app_tr.arb`. Turkish has no grammatical plural distinction for counted nouns, so pluralized strings use a single form with pipe separator as fallback:
```json
"shardNeedMore": "Sırrı yeniden oluşturmak için buna benzer {count} QR koda daha ihtiyacınız var | Sırrı yeniden oluşturmak için buna benzer {count} QR koda daha ihtiyacınız var"
```

- [ ] **Step 3: Create `src/locales/be.json`**

Port from `banana_split_flutter/lib/l10n/app_be.arb`. Belarusian uses the same Slavic plural rules as Russian (3 forms).

- [ ] **Step 4: Create `src/locales/ka.json`**

Port from `banana_split_flutter/lib/l10n/app_ka.arb`. Georgian typically uses a single plural form.

- [ ] **Step 5: Create `src/locales/uk.json`**

Port from `banana_split_flutter/lib/l10n/app_uk.arb`. Ukrainian uses the same Slavic plural rules as Russian (3 forms).

- [ ] **Step 6: Verify all locale files parse as valid JSON**

```bash
cd /home/mezinster/banana_split && for f in src/locales/*.json; do echo -n "$f: "; python3 -c "import json; json.load(open('$f')); print('OK')" 2>&1; done
```

Expected: All 6 files print "OK".

- [ ] **Step 7: Verify all locale files have the same keys as en.json**

```bash
cd /home/mezinster/banana_split && python3 -c "
import json, sys
en = json.load(open('src/locales/en.json'))
for lang in ['ru', 'tr', 'be', 'ka', 'uk']:
    other = json.load(open(f'src/locales/{lang}.json'))
    missing = set(en.keys()) - set(other.keys())
    extra = set(other.keys()) - set(en.keys())
    if missing: print(f'{lang}: MISSING {missing}')
    if extra: print(f'{lang}: EXTRA {extra}')
    if not missing and not extra: print(f'{lang}: OK')
"
```

Expected: All 5 locales print "OK".

- [ ] **Step 8: Build and lint**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn lint 2>&1 | tail -5 && yarn build 2>&1 | tail -5'
```

- [ ] **Step 9: Commit**

```bash
git add src/locales/
git commit -m "feat(i18n): add translations for RU, TR, BE, KA, UK locales"
```

---

## Chunk 4: Final Polish & Verification

### Task 8: End-to-end verification and CSS fixes

**Files:**
- Possibly modify: any files with remaining issues

- [ ] **Step 1: Run full lint**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn lint'
```

Fix any lint errors.

- [ ] **Step 2: Run unit tests**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn test:unit'
```

All 11 tests should pass.

- [ ] **Step 3: Build and verify the output is a single HTML file**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn build && ls -la dist/ && file dist/index.html'
```

Expected: `dist/index.html` is a single self-contained HTML file with all JS/CSS inlined (same as before i18n).

- [ ] **Step 4: Manual smoke test (dev server)**

```bash
cd /home/mezinster/banana_split && bash -c 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && yarn serve'
```

Verify in browser:
1. Language selector appears in header with flag emoji
2. Clicking a different language updates all UI text
3. Share view: all labels, placeholders, and buttons are translated
4. Combine view: all labels and error messages are translated
5. Print language selector appears next to "Print us!" button
6. Changing print language doesn't change UI language
7. GeneralInfo page content is fully translated
8. `html lang` attribute updates when switching language

- [ ] **Step 5: Commit any fixes**

```bash
git add -A
git commit -m "fix(i18n): polish and fix issues found during verification"
```

### Task 9: Update documentation

**Files:**
- Modify: `CLAUDE.md`
- Modify: `CHANGELOG.md`

- [ ] **Step 1: Update CLAUDE.md**

Add i18n section under the web app documentation:
- vue-i18n v8 setup
- How to add new strings (add key to all 6 locale files, use `$t()` in template)
- How to add a new language
- Print locale override pattern
- ShardInfo i18n injection requirement

- [ ] **Step 2: Update CHANGELOG.md**

Add entry for web app multilingual support.

- [ ] **Step 3: Commit**

```bash
git add CLAUDE.md CHANGELOG.md
git commit -m "docs: add web app i18n documentation and changelog entry"
```
