Repository: sillsdev/languageforge-lexbox
Pull request #1519: Chore/fw lite i18n
Author: login: hahn-kev
Created: 2025-03-05T06:06:09Z | Merged: 2025-03-10T07:36:17Z

starts progress on #1470 

super simple example of how it works, in your svelte file:
```<MenuItem>{$t`Logout`}</MenuItem>```
you type the english text straight in the html. Once you've done that, you run `pnpm run i18n:extract` this will pull the messages out into json files under `viewer/locales/en.json` or `fr.json` etc. 

Docs for using it https://github.com/HenryLie/svelte-i18n-lingui

Does not include:
- remembering the users locale and reusing it on next launch
- localized project view, only the home page is covered here

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced user interface with comprehensive multi-language support, now offering English, Spanish, and French.
  - Introduced a new language selection component that enables users to seamlessly switch locales.
  - Added internationalization support across various components, allowing dynamic text rendering based on user language preferences.
  - Implemented practical usage examples for the internationalization library in the documentation.

- **Documentation**
  - Revised documentation to focus on internationalization usage examples and instructions.

- **Chores**
  - Updated build configurations and development tooling to streamline translation extraction and integration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 83a292e771..6648774be6 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -312,6 +312,9 @@ importers:
 
   viewer:
     dependencies:
+      '@lingui/core':
+        specifier: ^5.2.0
+        version: 5.2.0(@lingui/babel-plugin-lingui-macro@5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3))(babel-plugin-macros@3.1.0)
       '@microsoft/dotnet-js-interop':
         specifier: ^8.0.0
         version: 8.0.0
@@ -336,6 +339,9 @@ importers:
       svelte-exmarkdown:
         specifier: 'catalog:'
         version: 4.0.3(svelte@5.20.0)
+      svelte-i18n-lingui:
+        specifier: ^0.2.2
+        version: 0.2.2(@lingui/cli@5.2.0(typescript@5.7.3))(@lingui/core@5.2.0(@lingui/babel-plugin-lingui-macro@5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3))(babel-plugin-macros@3.1.0))(svelte@5.20.0)(typescript@5.7.3)
       svelte-preprocess:
         specifier: 'catalog:'
         version: 6.0.3(@babel/core@7.26.8)(postcss-load-config@4.0.2(postcss@8.5.2))(postcss@8.5.2)(svelte@5.20.0)(typescript@5.7.3)
@@ -355,6 +361,15 @@ importers:
       '@iconify-json/mdi':
         specifier: ^1.1.66
         version: 1.2.3
+      '@lingui/cli':
+        specifier: ^5.2.0
+        version: 5.2.0(typescript@5.7.3)
+      '@lingui/format-json':
+        specifier: ^5.2.0
+        version: 5.2.0(typescript@5.7.3)
+      '@lingui/vite-plugin':
+        specifier: ^5.2.0
+        version: 5.2.0(typescript@5.7.3)(vite@6.1.0(@types/node@20.17.17)(jiti@2.4.2)(yaml@2.7.0))
       '@mdi/js':
         specifier: ^7.4.47
         version: 7.4.47
@@ -743,102 +758,204 @@ packages:
     resolution: {integrity: sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==}
     engines: {node: '>=18.0.0'}
 
+  '@esbuild/aix-ppc64@0.21.5':
+    resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [aix]
+
   '@esbuild/aix-ppc64@0.24.2':
     resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
     engines: {node: '>=18'}
     cpu: [ppc64]
     os: [aix]
 
+  '@esbuild/android-arm64@0.21.5':
+    resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [android]
+
   '@esbuild/android-arm64@0.24.2':
     resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [android]
 
+  '@esbuild/android-arm@0.21.5':
+    resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [android]
+
   '@esbuild/android-arm@0.24.2':
     resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
     engines: {node: '>=18'}
     cpu: [arm]
     os: [android]
 
+  '@esbuild/android-x64@0.21.5':
+    resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [android]
+
   '@esbuild/android-x64@0.24.2':
     resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [android]
 
+  '@esbuild/darwin-arm64@0.21.5':
+    resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [darwin]
+
   '@esbuild/darwin-arm64@0.24.2':
     resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [darwin]
 
+  '@esbuild/darwin-x64@0.21.5':
+    resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [darwin]
+
   '@esbuild/darwin-x64@0.24.2':
     resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [darwin]
 
+  '@esbuild/freebsd-arm64@0.21.5':
+    resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [freebsd]
+
   '@esbuild/freebsd-arm64@0.24.2':
     resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [freebsd]
 
+  '@esbuild/freebsd-x64@0.21.5':
+    resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [freebsd]
+
   '@esbuild/freebsd-x64@0.24.2':
     resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [freebsd]
 
+  '@esbuild/linux-arm64@0.21.5':
+    resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [linux]
+
   '@esbuild/linux-arm64@0.24.2':
     resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [linux]
 
+  '@esbuild/linux-arm@0.21.5':
+    resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [linux]
+
   '@esbuild/linux-arm@0.24.2':
     resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
     engines: {node: '>=18'}
     cpu: [arm]
     os: [linux]
 
+  '@esbuild/linux-ia32@0.21.5':
+    resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [linux]
+
   '@esbuild/linux-ia32@0.24.2':
     resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
     engines: {node: '>=18'}
     cpu: [ia32]
     os: [linux]
 
+  '@esbuild/linux-loong64@0.21.5':
+    resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
+    engines: {node: '>=12'}
+    cpu: [loong64]
+    os: [linux]
+
   '@esbuild/linux-loong64@0.24.2':
     resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
     engines: {node: '>=18'}
     cpu: [loong64]
     os: [linux]
 
+  '@esbuild/linux-mips64el@0.21.5':
+    resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
+    engines: {node: '>=12'}
+    cpu: [mips64el]
+    os: [linux]
+
   '@esbuild/linux-mips64el@0.24.2':
     resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
     engines: {node: '>=18'}
     cpu: [mips64el]
     os: [linux]
 
+  '@esbuild/linux-ppc64@0.21.5':
+    resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [linux]
+
   '@esbuild/linux-ppc64@0.24.2':
     resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
     engines: {node: '>=18'}
     cpu: [ppc64]
     os: [linux]
 
+  '@esbuild/linux-riscv64@0.21.5':
+    resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
+    engines: {node: '>=12'}
+    cpu: [riscv64]
+    os: [linux]
+
   '@esbuild/linux-riscv64@0.24.2':
     resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
     engines: {node: '>=18'}
     cpu: [riscv64]
     os: [linux]
 
+  '@esbuild/linux-s390x@0.21.5':
+    resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
+    engines: {node: '>=12'}
+    cpu: [s390x]
+    os: [linux]
+
   '@esbuild/linux-s390x@0.24.2':
     resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
     engines: {node: '>=18'}
     cpu: [s390x]
     os: [linux]
 
+  '@esbuild/linux-x64@0.21.5':
+    resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [linux]
+
   '@esbuild/linux-x64@0.24.2':
     resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
     engines: {node: '>=18'}
@@ -851,6 +968,12 @@ packages:
     cpu: [arm64]
     os: [netbsd]
 
+  '@esbuild/netbsd-x64@0.21.5':
+    resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [netbsd]
+
   '@esbuild/netbsd-x64@0.24.2':
     resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
     engines: {node: '>=18'}
@@ -863,30 +986,60 @@ packages:
     cpu: [arm64]
     os: [openbsd]
 
+  '@esbuild/openbsd-x64@0.21.5':
+    resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [openbsd]
+
   '@esbuild/openbsd-x64@0.24.2':
     resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [openbsd]
 
+  '@esbuild/sunos-x64@0.21.5':
+    resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [sunos]
+
   '@esbuild/sunos-x64@0.24.2':
     resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [sunos]
 
+  '@esbuild/win32-arm64@0.21.5':
+    resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [win32]
+
   '@esbuild/win32-arm64@0.24.2':
     resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [win32]
 
+  '@esbuild/win32-ia32@0.21.5':
+    resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [win32]
+
   '@esbuild/win32-ia32@0.24.2':
     resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
     engines: {node: '>=18'}
     cpu: [ia32]
     os: [win32]
 
+  '@esbuild/win32-x64@0.21.5':
+    resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [win32]
+
   '@esbuild/win32-x64@0.24.2':
     resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
     engines: {node: '>=18'}
@@ -1240,6 +1393,14 @@ packages:
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
 
+  '@jest/schemas@29.6.3':
+    resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+  '@jest/types@29.6.3':
+    resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
   '@jridgewell/gen-mapping@0.3.8':
     resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
     engines: {node: '>=6.0.0'}
@@ -1261,9 +1422,66 @@ packages:
   '@js-sdsl/ordered-map@4.4.2':
     resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
 
+  '@lingui/babel-plugin-extract-messages@5.2.0':
+    resolution: {integrity: sha512-hQ6tFK72ZXX2813PU9thJbnwJ+SjSrfR3/tt4aqHJcOUdrb67wMVY/0xiUe+vb5y6kVZjZ4oPqdgCfGZ2jWBEw==}
+    engines: {node: '>=20.0.0'}
+
+  '@lingui/babel-plugin-lingui-macro@5.2.0':
+    resolution: {integrity: sha512-IEpEfKW2WoGiK30dbovwXaPj69dKUP+GEAk00/6KUMB0sonaBWO4NO3Bj9G6NSdA6fB1lm9BtvuPqJQ2DvjF5g==}
+    engines: {node: '>=20.0.0'}
+    peerDependencies:
+      babel-plugin-macros: 2 || 3
+    peerDependenciesMeta:
+      babel-plugin-macros:
+        optional: true
+
+  '@lingui/cli@5.2.0':
+    resolution: {integrity: sha512-SLMPi9VMNAmhKRGt3HCGIZVHHmxfAcb7zNK9qwrEhlvcwxNmtsPtLb4iJvKy/VpdCQYm7C6D34tFjyVjUZ4ROg==}
+    engines: {node: '>=20.0.0'}
+    hasBin: true
+
+  '@lingui/conf@5.2.0':
+    resolution: {integrity: sha512-3biQJxGntCP+EnOe9jjlquGCBfk6ogq+I8ZduHwmBceY5aQ0OR7V23ItDrMz0NBy8dFNk5YoeHun3CYKYOS/Jg==}
+    engines: {node: '>=20.0.0'}
+
+  '@lingui/core@5.2.0':
+    resolution: {integrity: sha512-cz35uKDxIGb/CPvgwn7BM/QYpxtARmQm7n+mHUoNJdNKSrg9R7vKkLRG7k9dukZwix2Mdjh+2dPIJnAkor2CiA==}
+    engines: {node: '>=20.0.0'}
+    peerDependencies:
+      '@lingui/babel-plugin-lingui-macro': 5.2.0
+      babel-plugin-macros: 2 || 3
+    peerDependenciesMeta:
+      '@lingui/babel-plugin-lingui-macro':
+        optional: true
+      babel-plugin-macros:
+        optional: true
+
+  '@lingui/format-json@5.2.0':
+    resolution: {integrity: sha512-Km5KdDkPcHTQXZBNt1YhIAdAustPpfCB6o+7kCyRJqZ2uZE7uuC3EA2M6gTLXNTz8+1ynYIVGmy7zIgixcD4hw==}
+    engines: {node: '>=20.0.0'}
+
+  '@lingui/format-po@5.2.0':
+    resolution: {integrity: sha512-viUQaoRa8UxSghayTY7xjtwXbfXIVdlM8C4HsxmozQnl5TXnPVEwlaPYds3sdJ8PmQGcYCm35r8EsmuKBoWYDQ==}
+    engines: {node: '>=20.0.0'}
+
+  '@lingui/message-utils@5.2.0':
+    resolution: {integrity: sha512-qJFKNc1b7SRX6y5ywtA1x+2/gaY22e09hjC6fiDvDpAFdEguI4qAJGmBmqlAZG/kcokR0tmMpo9zYUF8jjcHEA==}
+    engines: {node: '>=20.0.0'}
+    bundledDependencies:
+      - '@messageformat/date-skeleton'
+
+  '@lingui/vite-plugin@5.2.0':
+    resolution: {integrity: sha512-jMpf6JJY1s3t4eFRBseTyuQNxy6ERRwg+uLi8EZ/qcaQgQW+GK6qWX/Qg5xQ8k1mJpaP6ihanMQMrkS6d5oR/A==}
+    engines: {node: '>=20.0.0'}
+    peerDependencies:
+      vite: ^3 || ^4 || ^5.0.9 || ^6
+
   '@mdi/js@7.4.47':
     resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==}
 
+  '@messageformat/parser@5.1.1':
+    resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==}
+
   '@microsoft/dotnet-js-interop@8.0.0':
     resolution: {integrity: sha512-REgH1LzhP68m3i8B80KLyKLRIOhLbWjwGl60HyVQEqN2eDeFUlgRfjifMa+Udl/NpIIafjqslMAouV8BUzHm8Q==}
 
@@ -2096,6 +2314,9 @@ packages:
   '@selderee/plugin-htmlparser2@0.11.0':
     resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
 
+  '@sinclair/typebox@0.27.8':
+    resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
+
   '@stylistic/eslint-plugin@2.13.0':
     resolution: {integrity: sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2243,6 +2464,15 @@ packages:
   '@types/http-errors@2.0.4':
     resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
 
+  '@types/istanbul-lib-coverage@2.0.6':
+    resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
+
+  '@types/istanbul-lib-report@3.0.3':
+    resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==}
+
+  '@types/istanbul-reports@3.0.4':
+    resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
+
   '@types/js-cookie@3.0.6':
     resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
 
@@ -2291,6 +2521,9 @@ packages:
   '@types/node@20.17.17':
     resolution: {integrity: sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==}
 
+  '@types/parse-json@4.0.2':
+    resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
+
   '@types/pg-pool@2.0.4':
     resolution: {integrity: sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==}
 
@@ -2336,6 +2569,12 @@ packages:
   '@types/ws@8.5.14':
     resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==}
 
+  '@types/yargs-parser@21.0.3':
+    resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
+
+  '@types/yargs@17.0.33':
+    resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
+
   '@types/zxcvbn@4.4.5':
     resolution: {integrity: sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==}
 
@@ -2605,6 +2844,10 @@ packages:
     resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
     engines: {node: '>= 0.4'}
 
+  babel-plugin-macros@3.1.0:
+    resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
+    engines: {node: '>=10', npm: '>=6'}
+
   babel-plugin-precompile-intl@0.5.2:
     resolution: {integrity: sha512-sTXC+8+krOCP72euH47HqUZ0RAc/mcNA7UoX5joPb5J+dladwOwVcDQMWdv8MflGuWW0PkvAwXdQQWF+/L3Qxg==}
     peerDependencies:
@@ -2690,6 +2933,10 @@ packages:
     resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
     engines: {node: '>= 6'}
 
+  camelcase@6.3.0:
+    resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+    engines: {node: '>=10'}
+
   caniuse-lite@1.0.30001699:
     resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==}
 
@@ -2734,6 +2981,10 @@ packages:
     resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
     engines: {node: '>= 6'}
 
+  chokidar@3.5.1:
+    resolution: {integrity: sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==}
+    engines: {node: '>= 8.10.0'}
+
   chokidar@3.6.0:
     resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
     engines: {node: '>= 8.10.0'}
@@ -2761,6 +3012,10 @@ packages:
     resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
     engines: {node: '>=6'}
 
+  cli-table@0.3.11:
+    resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==}
+    engines: {node: '>= 0.2.0'}
+
   cli-truncate@2.1.0:
     resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
     engines: {node: '>=8'}
@@ -2814,6 +3069,10 @@ packages:
   colorette@2.0.20:
     resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
 
+  colors@1.0.3:
+    resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==}
+    engines: {node: '>=0.1.90'}
+
   colorspace@1.1.4:
     resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
 
@@ -2877,6 +3136,10 @@ packages:
     resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
     engines: {node: '>= 0.6'}
 
+  cosmiconfig@7.1.0:
+    resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
+    engines: {node: '>=10'}
+
   cosmiconfig@8.3.6:
     resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
     engines: {node: '>=14'}
@@ -2972,6 +3235,9 @@ packages:
   dataloader@2.2.3:
     resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==}
 
+  date-fns@3.6.0:
+    resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
+
   date-fns@4.1.0:
     resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
 
@@ -3153,6 +3419,11 @@ packages:
   es-module-lexer@1.6.0:
     resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==}
 
+  esbuild@0.21.5:
+    resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
+    engines: {node: '>=12'}
+    hasBin: true
+
   esbuild@0.24.2:
     resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
     engines: {node: '>=18'}
@@ -3246,6 +3517,9 @@ packages:
     resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
     engines: {node: '>=4.0'}
 
+  estree-walker-ts@1.1.0:
+    resolution: {integrity: sha512-qZDD6u86YMB15jps0ZYDE7fxJl2EZsxspagUcLg6K3k/WKRkGGpXM/5kk5a2UvX5+yv6viQ1jYYr1De4IiBx/w==}
+
   estree-walker@1.0.1:
     resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
 
@@ -3444,6 +3718,11 @@ packages:
     resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
     hasBin: true
 
+  glob@11.0.1:
+    resolution: {integrity: sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==}
+    engines: {node: 20 || >=22}
+    hasBin: true
+
   globals@11.12.0:
     resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
     engines: {node: '>=4'}
@@ -3623,6 +3902,10 @@ packages:
   ini@1.3.8:
     resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
 
+  inquirer@7.3.3:
+    resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==}
+    engines: {node: '>=8.0.0'}
+
   inquirer@8.2.6:
     resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==}
     engines: {node: '>=12.0.0'}
@@ -3743,6 +4026,18 @@ packages:
   jackspeak@3.4.3:
     resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
 
+  jackspeak@4.1.0:
+    resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==}
+    engines: {node: 20 || >=22}
+
+  jest-get-type@29.6.3:
+    resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+  jest-validate@29.7.0:
+    resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
   jiti@1.21.7:
     resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
     hasBin: true
@@ -3766,6 +4061,9 @@ packages:
     resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
     engines: {node: '>=14'}
 
+  js-sha256@0.10.1:
+    resolution: {integrity: sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==}
+
   js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
@@ -3846,6 +4144,10 @@ packages:
   leac@0.6.0:
     resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
 
+  leven@3.1.0:
+    resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
+    engines: {node: '>=6'}
+
   levn@0.4.1:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
     engines: {node: '>= 0.8.0'}
@@ -3927,6 +4229,10 @@ packages:
   lodash.foreach@4.5.0:
     resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
 
+  lodash.get@4.4.2:
+    resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
+    deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
+
   lodash.isplainobject@4.0.6:
     resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
 
@@ -3982,6 +4288,10 @@ packages:
   lru-cache@10.4.3:
     resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
 
+  lru-cache@11.0.2:
+    resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==}
+    engines: {node: 20 || >=22}
+
   lru-cache@5.1.1:
     resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
 
@@ -4176,6 +4486,10 @@ packages:
     resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
     engines: {node: '>=4'}
 
+  minimatch@10.0.1:
+    resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
+    engines: {node: 20 || >=22}
+
   minimatch@3.1.2:
     resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
 
@@ -4296,6 +4610,9 @@ packages:
   module-details-from-path@1.0.3:
     resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==}
 
+  moo@0.5.2:
+    resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
+
   mri@1.2.0:
     resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
     engines: {node: '>=4'}
@@ -4510,6 +4827,10 @@ packages:
     resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
     engines: {node: '>=16 || 14 >=14.18'}
 
+  path-scurry@2.0.0:
+    resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
+    engines: {node: 20 || >=22}
+
   path-starts-with@2.0.1:
     resolution: {integrity: sha512-wZ3AeiRBRlNwkdUxvBANh0+esnt38DLffHDujZyRHkqkaKHTglnY2EP5UX3b8rdeiSutgO4y9NEJwXezNP5vHg==}
     engines: {node: '>=8'}
@@ -4518,6 +4839,9 @@ packages:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
 
+  pathe@1.1.2:
+    resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
   pathe@2.0.3:
     resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
 
@@ -4578,6 +4902,9 @@ packages:
     engines: {node: '>=18'}
     hasBin: true
 
+  pofile@1.1.4:
+    resolution: {integrity: sha512-r6Q21sKsY1AjTVVjOuU02VYKVNQGJNQHjTIvs4dEbeuuYfxgYk/DGD2mqqq4RDaVkwdSq0VEtmQUOPe/wH8X3g==}
+
   postcss-import@15.1.0:
     resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
     engines: {node: '>=14.0.0'}
@@ -4685,6 +5012,10 @@ packages:
     resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
 
+  pretty-format@29.7.0:
+    resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
   prism-svelte@0.5.0:
     resolution: {integrity: sha512-db91Bf3pRGKDPz1lAqLFSJXeW13mulUJxhycysFpfXV5MIK7RgWWK2E5aPAa71s8TCzQUXxF5JOV42/iOs6QkA==}
 
@@ -4711,6 +5042,11 @@ packages:
   proxy-from-env@1.1.0:
     resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
 
+  pseudolocale@2.1.0:
+    resolution: {integrity: sha512-af5fsrRvVwD+MBasBJvuDChT0KDqT0nEwD9NTgbtHJ16FKomWac9ua0z6YVNB4G9x9IOaiGWym62aby6n4tFMA==}
+    engines: {node: '>=16.0.0'}
+    hasBin: true
+
   psl@1.15.0:
     resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
 
@@ -4731,6 +5067,9 @@ packages:
   react-is@17.0.2:
     resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
 
+  react-is@18.3.1:
+    resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
   read-cache@1.0.0:
     resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
 
@@ -4738,6 +5077,10 @@ packages:
     resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
     engines: {node: '>= 6'}
 
+  readdirp@3.5.0:
+    resolution: {integrity: sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==}
+    engines: {node: '>=8.10.0'}
+
   readdirp@3.6.0:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
@@ -4860,6 +5203,10 @@ packages:
   run-parallel@1.2.0:
     resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
 
+  rxjs@6.6.7:
+    resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==}
+    engines: {npm: '>=2.0.0'}
+
   rxjs@7.8.1:
     resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
 
@@ -4962,6 +5309,10 @@ packages:
     resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
     engines: {node: '>=0.10.0'}
 
+  source-map@0.8.0-beta.0:
+    resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
+    engines: {node: '>= 8'}
+
   sponge-case@1.0.1:
     resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==}
 
@@ -5075,6 +5426,13 @@ packages:
     peerDependencies:
       svelte: ^3.19.0 || ^4.0.0
 
+  svelte-i18n-lingui@0.2.2:
+    resolution: {integrity: sha512-HxVsn56T4qt6ScNhaMBvkSv1Wy962NZUXjmXe7SnHZzXrkHgK2Is6zwa3knaXP5u2t/mSf/cTemOcEFTBBpnww==}
+    peerDependencies:
+      '@lingui/cli': '>=4'
+      '@lingui/core': '>=4'
+      svelte: '>=4'
+
   svelte-intl-precompile@0.12.3:
     resolution: {integrity: sha512-/AA4io2O07h8PzDU8Jg5ab/DBDr6C2KOzyu4CWSsjHjm8pxWtkLDhd7XkNema34Ryl7O3oqjVrH8FsjLAEUXGQ==}
 
@@ -5232,6 +5590,9 @@ packages:
   tr46@0.0.3:
     resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
 
+  tr46@1.0.1:
+    resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
+
   trim-lines@3.0.1:
     resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
 
@@ -5254,6 +5615,9 @@ packages:
   ts-log@2.2.7:
     resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==}
 
+  tslib@1.14.1:
+    resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
+
   tslib@2.6.3:
     resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
 
@@ -5345,6 +5709,9 @@ packages:
     resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==}
     engines: {node: '>=0.10.0'}
 
+  unraw@3.0.0:
+    resolution: {integrity: sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==}
+
   update-browserslist-db@1.1.2:
     resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==}
     hasBin: true
@@ -5500,6 +5867,9 @@ packages:
   webidl-conversions@3.0.1:
     resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
 
+  webidl-conversions@4.0.2:
+    resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
+
   webidl-conversions@7.0.0:
     resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
     engines: {node: '>=12'}
@@ -5515,6 +5885,9 @@ packages:
   whatwg-url@5.0.0:
     resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
 
+  whatwg-url@7.1.0:
+    resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
+
   which@2.0.2:
     resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
     engines: {node: '>= 8'}
@@ -6045,78 +6418,147 @@ snapshots:
     dependencies:
       tslib: 2.8.1
 
+  '@esbuild/aix-ppc64@0.21.5':
+    optional: true
+
   '@esbuild/aix-ppc64@0.24.2':
     optional: true
 
+  '@esbuild/android-arm64@0.21.5':
+    optional: true
+
   '@esbuild/android-arm64@0.24.2':
     optional: true
 
+  '@esbuild/android-arm@0.21.5':
+    optional: true
+
   '@esbuild/android-arm@0.24.2':
     optional: true
 
+  '@esbuild/android-x64@0.21.5':
+    optional: true
+
   '@esbuild/android-x64@0.24.2':
     optional: true
 
+  '@esbuild/darwin-arm64@0.21.5':
+    optional: true
+
   '@esbuild/darwin-arm64@0.24.2':
     optional: true
 
+  '@esbuild/darwin-x64@0.21.5':
+    optional: true
+
   '@esbuild/darwin-x64@0.24.2':
     optional: true
 
+  '@esbuild/freebsd-arm64@0.21.5':
+    optional: true
+
   '@esbuild/freebsd-arm64@0.24.2':
     optional: true
 
+  '@esbuild/freebsd-x64@0.21.5':
+    optional: true
+
   '@esbuild/freebsd-x64@0.24.2':
     optional: true
 
+  '@esbuild/linux-arm64@0.21.5':
+    optional: true
+
   '@esbuild/linux-arm64@0.24.2':
     optional: true
 
+  '@esbuild/linux-arm@0.21.5':
+    optional: true
+
   '@esbuild/linux-arm@0.24.2':
     optional: true
 
+  '@esbuild/linux-ia32@0.21.5':
+    optional: true
+
   '@esbuild/linux-ia32@0.24.2':
     optional: true
 
+  '@esbuild/linux-loong64@0.21.5':
+    optional: true
+
   '@esbuild/linux-loong64@0.24.2':
     optional: true
 
+  '@esbuild/linux-mips64el@0.21.5':
+    optional: true
+
   '@esbuild/linux-mips64el@0.24.2':
     optional: true
 
+  '@esbuild/linux-ppc64@0.21.5':
+    optional: true
+
   '@esbuild/linux-ppc64@0.24.2':
     optional: true
 
+  '@esbuild/linux-riscv64@0.21.5':
+    optional: true
+
   '@esbuild/linux-riscv64@0.24.2':
     optional: true
 
+  '@esbuild/linux-s390x@0.21.5':
+    optional: true
+
   '@esbuild/linux-s390x@0.24.2':
     optional: true
 
+  '@esbuild/linux-x64@0.21.5':
+    optional: true
+
   '@esbuild/linux-x64@0.24.2':
     optional: true
 
   '@esbuild/netbsd-arm64@0.24.2':
     optional: true
 
+  '@esbuild/netbsd-x64@0.21.5':
+    optional: true
+
   '@esbuild/netbsd-x64@0.24.2':
     optional: true
 
   '@esbuild/openbsd-arm64@0.24.2':
     optional: true
 
+  '@esbuild/openbsd-x64@0.21.5':
+    optional: true
+
   '@esbuild/openbsd-x64@0.24.2':
     optional: true
 
+  '@esbuild/sunos-x64@0.21.5':
+    optional: true
+
   '@esbuild/sunos-x64@0.24.2':
     optional: true
 
+  '@esbuild/win32-arm64@0.21.5':
+    optional: true
+
   '@esbuild/win32-arm64@0.24.2':
     optional: true
 
+  '@esbuild/win32-ia32@0.21.5':
+    optional: true
+
   '@esbuild/win32-ia32@0.24.2':
     optional: true
 
+  '@esbuild/win32-x64@0.21.5':
+    optional: true
+
   '@esbuild/win32-x64@0.24.2':
     optional: true
 
@@ -6731,6 +7173,19 @@ snapshots:
       wrap-ansi: 8.1.0
       wrap-ansi-cjs: wrap-ansi@7.0.0
 
+  '@jest/schemas@29.6.3':
+    dependencies:
+      '@sinclair/typebox': 0.27.8
+
+  '@jest/types@29.6.3':
+    dependencies:
+      '@jest/schemas': 29.6.3
+      '@types/istanbul-lib-coverage': 2.0.6
+      '@types/istanbul-reports': 3.0.4
+      '@types/node': 20.17.17
+      '@types/yargs': 17.0.33
+      chalk: 4.1.2
+
   '@jridgewell/gen-mapping@0.3.8':
     dependencies:
       '@jridgewell/set-array': 1.2.1
@@ -6750,8 +7205,112 @@ snapshots:
 
   '@js-sdsl/ordered-map@4.4.2': {}
 
+  '@lingui/babel-plugin-extract-messages@5.2.0': {}
+
+  '@lingui/babel-plugin-lingui-macro@5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3)':
+    dependencies:
+      '@babel/core': 7.26.8
+      '@babel/runtime': 7.26.7
+      '@babel/types': 7.26.8
+      '@lingui/conf': 5.2.0(typescript@5.7.3)
+      '@lingui/core': 5.2.0(@lingui/babel-plugin-lingui-macro@5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3))(babel-plugin-macros@3.1.0)
+      '@lingui/message-utils': 5.2.0
+    optionalDependencies:
+      babel-plugin-macros: 3.1.0
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
+  '@lingui/cli@5.2.0(typescript@5.7.3)':
+    dependencies:
+      '@babel/core': 7.26.8
+      '@babel/generator': 7.26.8
+      '@babel/parser': 7.26.8
+      '@babel/runtime': 7.26.7
+      '@babel/types': 7.26.8
+      '@lingui/babel-plugin-extract-messages': 5.2.0
+      '@lingui/babel-plugin-lingui-macro': 5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3)
+      '@lingui/conf': 5.2.0(typescript@5.7.3)
+      '@lingui/core': 5.2.0(@lingui/babel-plugin-lingui-macro@5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3))(babel-plugin-macros@3.1.0)
+      '@lingui/format-po': 5.2.0(typescript@5.7.3)
+      '@lingui/message-utils': 5.2.0
+      babel-plugin-macros: 3.1.0
+      chalk: 4.1.2
+      chokidar: 3.5.1
+      cli-table: 0.3.11
+      commander: 10.0.1
+      convert-source-map: 2.0.0
+      date-fns: 3.6.0
+      esbuild: 0.21.5
+      glob: 11.0.1
+      inquirer: 7.3.3
+      micromatch: 4.0.8
+      normalize-path: 3.0.0
+      ora: 5.4.1
+      pathe: 1.1.2
+      pkg-up: 3.1.0
+      pofile: 1.1.4
+      pseudolocale: 2.1.0
+      source-map: 0.8.0-beta.0
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
+  '@lingui/conf@5.2.0(typescript@5.7.3)':
+    dependencies:
+      '@babel/runtime': 7.26.7
+      chalk: 4.1.2
+      cosmiconfig: 8.3.6(typescript@5.7.3)
+      jest-validate: 29.7.0
+      jiti: 1.21.7
+      lodash.get: 4.4.2
+    transitivePeerDependencies:
+      - typescript
+
+  '@lingui/core@5.2.0(@lingui/babel-plugin-lingui-macro@5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3))(babel-plugin-macros@3.1.0)':
+    dependencies:
+      '@babel/runtime': 7.26.7
+      '@lingui/message-utils': 5.2.0
+      unraw: 3.0.0
+    optionalDependencies:
+      '@lingui/babel-plugin-lingui-macro': 5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3)
+      babel-plugin-macros: 3.1.0
+
+  '@lingui/format-json@5.2.0(typescript@5.7.3)':
+    dependencies:
+      '@lingui/conf': 5.2.0(typescript@5.7.3)
+    transitivePeerDependencies:
+      - typescript
+
+  '@lingui/format-po@5.2.0(typescript@5.7.3)':
+    dependencies:
+      '@lingui/conf': 5.2.0(typescript@5.7.3)
+      '@lingui/message-utils': 5.2.0
+      date-fns: 3.6.0
+      pofile: 1.1.4
+    transitivePeerDependencies:
+      - typescript
+
+  '@lingui/message-utils@5.2.0':
+    dependencies:
+      '@messageformat/parser': 5.1.1
+      js-sha256: 0.10.1
+
+  '@lingui/vite-plugin@5.2.0(typescript@5.7.3)(vite@6.1.0(@types/node@20.17.17)(jiti@2.4.2)(yaml@2.7.0))':
+    dependencies:
+      '@lingui/cli': 5.2.0(typescript@5.7.3)
+      '@lingui/conf': 5.2.0(typescript@5.7.3)
+      vite: 6.1.0(@types/node@20.17.17)(jiti@2.4.2)(yaml@2.7.0)
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
   '@mdi/js@7.4.47': {}
 
+  '@messageformat/parser@5.1.1':
+    dependencies:
+      moo: 0.5.2
+
   '@microsoft/dotnet-js-interop@8.0.0': {}
 
   '@microsoft/signalr@8.0.7':
@@ -7791,6 +8350,8 @@ snapshots:
       domhandler: 5.0.3
       selderee: 0.11.0
 
+  '@sinclair/typebox@0.27.8': {}
+
   '@stylistic/eslint-plugin@2.13.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)':
     dependencies:
       '@typescript-eslint/utils': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
@@ -7989,6 +8550,16 @@ snapshots:
 
   '@types/http-errors@2.0.4': {}
 
+  '@types/istanbul-lib-coverage@2.0.6': {}
+
+  '@types/istanbul-lib-report@3.0.3':
+    dependencies:
+      '@types/istanbul-lib-coverage': 2.0.6
+
+  '@types/istanbul-reports@3.0.4':
+    dependencies:
+      '@types/istanbul-lib-report': 3.0.3
+
   '@types/js-cookie@3.0.6': {}
 
   '@types/js-yaml@4.0.9': {}
@@ -8046,6 +8617,8 @@ snapshots:
     dependencies:
       undici-types: 6.19.8
 
+  '@types/parse-json@4.0.2': {}
+
   '@types/pg-pool@2.0.4':
     dependencies:
       '@types/pg': 8.6.1
@@ -8097,6 +8670,12 @@ snapshots:
     dependencies:
       '@types/node': 20.17.17
 
+  '@types/yargs-parser@21.0.3': {}
+
+  '@types/yargs@17.0.33':
+    dependencies:
+      '@types/yargs-parser': 21.0.3
+
   '@types/zxcvbn@4.4.5': {}
 
   '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)':
@@ -8393,6 +8972,12 @@ snapshots:
 
   axobject-query@4.1.0: {}
 
+  babel-plugin-macros@3.1.0:
+    dependencies:
+      '@babel/runtime': 7.26.7
+      cosmiconfig: 7.1.0
+      resolve: 1.22.10
+
   babel-plugin-precompile-intl@0.5.2(@babel/core@7.26.8):
     dependencies:
       '@babel/core': 7.26.8
@@ -8505,6 +9090,8 @@ snapshots:
 
   camelcase-css@2.0.1: {}
 
+  camelcase@6.3.0: {}
+
   caniuse-lite@1.0.30001699: {}
 
   capital-case@1.0.4:
@@ -8586,6 +9173,18 @@ snapshots:
       parse5: 7.2.1
       parse5-htmlparser2-tree-adapter: 7.1.0
 
+  chokidar@3.5.1:
+    dependencies:
+      anymatch: 3.1.3
+      braces: 3.0.3
+      glob-parent: 5.1.2
+      is-binary-path: 2.1.0
+      is-glob: 4.0.3
+      normalize-path: 3.0.0
+      readdirp: 3.5.0
+    optionalDependencies:
+      fsevents: 2.3.3
+
   chokidar@3.6.0:
     dependencies:
       anymatch: 3.1.3
@@ -8616,6 +9215,10 @@ snapshots:
 
   cli-spinners@2.9.2: {}
 
+  cli-table@0.3.11:
+    dependencies:
+      colors: 1.0.3
+
   cli-truncate@2.1.0:
     dependencies:
       slice-ansi: 3.0.0
@@ -8672,6 +9275,8 @@ snapshots:
 
   colorette@2.0.20: {}
 
+  colors@1.0.3: {}
+
   colorspace@1.1.4:
     dependencies:
       color: 3.2.1
@@ -8734,6 +9339,14 @@ snapshots:
 
   cookie@0.6.0: {}
 
+  cosmiconfig@7.1.0:
+    dependencies:
+      '@types/parse-json': 4.0.2
+      import-fresh: 3.3.1
+      parse-json: 5.2.0
+      path-type: 4.0.0
+      yaml: 1.10.2
+
   cosmiconfig@8.3.6(typescript@5.7.3):
     dependencies:
       import-fresh: 3.3.1
@@ -8832,6 +9445,8 @@ snapshots:
 
   dataloader@2.2.3: {}
 
+  date-fns@3.6.0: {}
+
   date-fns@4.1.0: {}
 
   debounce-fn@4.0.0:
@@ -8987,6 +9602,32 @@ snapshots:
 
   es-module-lexer@1.6.0: {}
 
+  esbuild@0.21.5:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.21.5
+      '@esbuild/android-arm': 0.21.5
+      '@esbuild/android-arm64': 0.21.5
+      '@esbuild/android-x64': 0.21.5
+      '@esbuild/darwin-arm64': 0.21.5
+      '@esbuild/darwin-x64': 0.21.5
+      '@esbuild/freebsd-arm64': 0.21.5
+      '@esbuild/freebsd-x64': 0.21.5
+      '@esbuild/linux-arm': 0.21.5
+      '@esbuild/linux-arm64': 0.21.5
+      '@esbuild/linux-ia32': 0.21.5
+      '@esbuild/linux-loong64': 0.21.5
+      '@esbuild/linux-mips64el': 0.21.5
+      '@esbuild/linux-ppc64': 0.21.5
+      '@esbuild/linux-riscv64': 0.21.5
+      '@esbuild/linux-s390x': 0.21.5
+      '@esbuild/linux-x64': 0.21.5
+      '@esbuild/netbsd-x64': 0.21.5
+      '@esbuild/openbsd-x64': 0.21.5
+      '@esbuild/sunos-x64': 0.21.5
+      '@esbuild/win32-arm64': 0.21.5
+      '@esbuild/win32-ia32': 0.21.5
+      '@esbuild/win32-x64': 0.21.5
+
   esbuild@0.24.2:
     optionalDependencies:
       '@esbuild/aix-ppc64': 0.24.2
@@ -9132,6 +9773,8 @@ snapshots:
 
   estraverse@5.3.0: {}
 
+  estree-walker-ts@1.1.0: {}
+
   estree-walker@1.0.1: {}
 
   estree-walker@2.0.2: {}
@@ -9332,6 +9975,15 @@ snapshots:
       package-json-from-dist: 1.0.1
       path-scurry: 1.11.1
 
+  glob@11.0.1:
+    dependencies:
+      foreground-child: 3.3.0
+      jackspeak: 4.1.0
+      minimatch: 10.0.1
+      minipass: 7.1.2
+      package-json-from-dist: 1.0.1
+      path-scurry: 2.0.0
+
   globals@11.12.0: {}
 
   globals@13.24.0:
@@ -9524,6 +10176,22 @@ snapshots:
 
   ini@1.3.8: {}
 
+  inquirer@7.3.3:
+    dependencies:
+      ansi-escapes: 4.3.2
+      chalk: 4.1.2
+      cli-cursor: 3.1.0
+      cli-width: 3.0.0
+      external-editor: 3.1.0
+      figures: 3.2.0
+      lodash: 4.17.21
+      mute-stream: 0.0.8
+      run-async: 2.4.1
+      rxjs: 6.6.7
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+      through: 2.3.8
+
   inquirer@8.2.6:
     dependencies:
       ansi-escapes: 4.3.2
@@ -9637,6 +10305,21 @@ snapshots:
     optionalDependencies:
       '@pkgjs/parseargs': 0.11.0
 
+  jackspeak@4.1.0:
+    dependencies:
+      '@isaacs/cliui': 8.0.2
+
+  jest-get-type@29.6.3: {}
+
+  jest-validate@29.7.0:
+    dependencies:
+      '@jest/types': 29.6.3
+      camelcase: 6.3.0
+      chalk: 4.1.2
+      jest-get-type: 29.6.3
+      leven: 3.1.0
+      pretty-format: 29.7.0
+
   jiti@1.21.7: {}
 
   jiti@2.4.2: {}
@@ -9655,6 +10338,8 @@ snapshots:
 
   js-cookie@3.0.5: {}
 
+  js-sha256@0.10.1: {}
+
   js-tokens@4.0.0: {}
 
   js-yaml@4.1.0:
@@ -9718,6 +10403,8 @@ snapshots:
 
   leac@0.6.0: {}
 
+  leven@3.1.0: {}
+
   levn@0.4.1:
     dependencies:
       prelude-ls: 1.2.1
@@ -9798,6 +10485,8 @@ snapshots:
 
   lodash.foreach@4.5.0: {}
 
+  lodash.get@4.4.2: {}
+
   lodash.isplainobject@4.0.6: {}
 
   lodash.merge@4.6.2: {}
@@ -9856,6 +10545,8 @@ snapshots:
 
   lru-cache@10.4.3: {}
 
+  lru-cache@11.0.2: {}
+
   lru-cache@5.1.1:
     dependencies:
       yallist: 3.1.1
@@ -10225,6 +10916,10 @@ snapshots:
 
   min-indent@1.0.1: {}
 
+  minimatch@10.0.1:
+    dependencies:
+      brace-expansion: 2.0.1
+
   minimatch@3.1.2:
     dependencies:
       brace-expansion: 1.1.11
@@ -10545,6 +11240,8 @@ snapshots:
 
   module-details-from-path@1.0.3: {}
 
+  moo@0.5.2: {}
+
   mri@1.2.0: {}
 
   mrmime@2.0.0: {}
@@ -10752,10 +11449,17 @@ snapshots:
       lru-cache: 10.4.3
       minipass: 7.1.2
 
+  path-scurry@2.0.0:
+    dependencies:
+      lru-cache: 11.0.2
+      minipass: 7.1.2
+
   path-starts-with@2.0.1: {}
 
   path-type@4.0.0: {}
 
+  pathe@1.1.2: {}
+
   pathe@2.0.3: {}
 
   pathval@2.0.0: {}
@@ -10808,6 +11512,8 @@ snapshots:
     optionalDependencies:
       fsevents: 2.3.2
 
+  pofile@1.1.4: {}
+
   postcss-import@15.1.0(postcss@8.5.2):
     dependencies:
       postcss: 8.5.2
@@ -10894,6 +11600,12 @@ snapshots:
       ansi-styles: 5.2.0
       react-is: 17.0.2
 
+  pretty-format@29.7.0:
+    dependencies:
+      '@jest/schemas': 29.6.3
+      ansi-styles: 5.2.0
+      react-is: 18.3.1
+
   prism-svelte@0.5.0: {}
 
   prism-themes@1.9.0: {}
@@ -10929,6 +11641,10 @@ snapshots:
 
   proxy-from-env@1.1.0: {}
 
+  pseudolocale@2.1.0:
+    dependencies:
+      commander: 10.0.1
+
   psl@1.15.0:
     dependencies:
       punycode: 2.3.1
@@ -10943,6 +11659,8 @@ snapshots:
 
   react-is@17.0.2: {}
 
+  react-is@18.3.1: {}
+
   read-cache@1.0.0:
     dependencies:
       pify: 2.3.0
@@ -10953,6 +11671,10 @@ snapshots:
       string_decoder: 1.3.0
       util-deprecate: 1.0.2
 
+  readdirp@3.5.0:
+    dependencies:
+      picomatch: 2.3.1
+
   readdirp@3.6.0:
     dependencies:
       picomatch: 2.3.1
@@ -11101,6 +11823,10 @@ snapshots:
     dependencies:
       queue-microtask: 1.2.3
 
+  rxjs@6.6.7:
+    dependencies:
+      tslib: 1.14.1
+
   rxjs@7.8.1:
     dependencies:
       tslib: 2.8.1
@@ -11188,6 +11914,10 @@ snapshots:
 
   source-map@0.6.1: {}
 
+  source-map@0.8.0-beta.0:
+    dependencies:
+      whatwg-url: 7.1.0
+
   sponge-case@1.0.1:
     dependencies:
       tslib: 2.8.1
@@ -11326,6 +12056,18 @@ snapshots:
     dependencies:
       svelte: 5.20.0
 
+  svelte-i18n-lingui@0.2.2(@lingui/cli@5.2.0(typescript@5.7.3))(@lingui/core@5.2.0(@lingui/babel-plugin-lingui-macro@5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3))(babel-plugin-macros@3.1.0))(svelte@5.20.0)(typescript@5.7.3):
+    dependencies:
+      '@lingui/cli': 5.2.0(typescript@5.7.3)
+      '@lingui/core': 5.2.0(@lingui/babel-plugin-lingui-macro@5.2.0(babel-plugin-macros@3.1.0)(typescript@5.7.3))(babel-plugin-macros@3.1.0)
+      '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
+      estree-walker-ts: 1.1.0
+      js-sha256: 0.10.1
+      svelte: 5.20.0
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
   svelte-intl-precompile@0.12.3(@babel/core@7.26.8)(svelte@5.20.0):
     dependencies:
       babel-plugin-precompile-intl: 0.5.2(@babel/core@7.26.8)
@@ -11543,6 +12285,10 @@ snapshots:
 
   tr46@0.0.3: {}
 
+  tr46@1.0.1:
+    dependencies:
+      punycode: 2.3.1
+
   trim-lines@3.0.1: {}
 
   triple-beam@1.4.1: {}
@@ -11557,6 +12303,8 @@ snapshots:
 
   ts-log@2.2.7: {}
 
+  tslib@1.14.1: {}
+
   tslib@2.6.3: {}
 
   tslib@2.8.1: {}
@@ -11647,6 +12395,8 @@ snapshots:
     dependencies:
       normalize-path: 2.1.1
 
+  unraw@3.0.0: {}
+
   update-browserslist-db@1.1.2(browserslist@4.24.4):
     dependencies:
       browserslist: 4.24.4
@@ -11799,6 +12549,8 @@ snapshots:
 
   webidl-conversions@3.0.1: {}
 
+  webidl-conversions@4.0.2: {}
+
   webidl-conversions@7.0.0: {}
 
   whatwg-mimetype@3.0.0: {}
@@ -11810,6 +12562,12 @@ snapshots:
       tr46: 0.0.3
       webidl-conversions: 3.0.1
 
+  whatwg-url@7.1.0:
+    dependencies:
+      lodash.sortby: 4.7.0
+      tr46: 1.0.1
+      webidl-conversions: 4.0.2
+
   which@2.0.2:
     dependencies:
       isexe: 2.0.0
diff --git a/frontend/viewer/README.md b/frontend/viewer/README.md
index e6cd94fce7..dd0114327d 100644
--- a/frontend/viewer/README.md
+++ b/frontend/viewer/README.md
@@ -1,47 +1,24 @@
-# Svelte + TS + Vite
 
-This template should help get you started developing with Svelte and TypeScript in Vite.
 
-## Recommended IDE Setup
+### i18n 
 
-[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
+We're using a different library here from the lexbox UI. The core lib is https://lingui.dev/
+with a Svelte plugin https://github.com/HenryLie/svelte-i18n-lingui
 
-## Need an official Svelte framework?
-
-Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
-
-## Technical considerations
-
-**Why use this over SvelteKit?**
-
-- It brings its own routing solution which might not be preferable for some users.
-- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
-
-This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
-
-Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
-
-**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
-
-Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
-
-**Why include `.vscode/extensions.json`?**
-
-Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
-
-**Why enable `allowJs` in the TS template?**
-
-While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
-
-**Why is HMR not preserving my local component state?**
-
-HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
+Basic usage looks like this:
+```sveltehtml
+<span>{$t`Logout`}</span>
+```
+for the english text 'Logout'. Then to make localization files run:
+```bash
+pnpm run i18n:extract
+```
+this will update the files under /src/locales/
+if you want you can then feed those files to an AI and it'll translate them for you.
 
-If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
+#### advanced usage
 
-```ts
-// store.ts
-// An extremely simple external store
-import { writable } from 'svelte/store'
-export default writable(0)
+for formatted values you can do this:
+```sveltehtml
+<span>{$t`Hello ${name}, how are you today?`}</span>
 ```
diff --git a/frontend/viewer/eslint.config.js b/frontend/viewer/eslint.config.js
index 219ffd4726..1ce9d15049 100644
--- a/frontend/viewer/eslint.config.js
+++ b/frontend/viewer/eslint.config.js
@@ -117,7 +117,9 @@ export default [
     languageOptions: {
       parser: tsParser,
       parserOptions: {
-        project: true,
+        projectService: {
+          allowDefaultProject: ['lingui.config.ts'],
+        },
         tsconfigRootDir: __dirname,
         extraFileExtensions: ['.svelte'], // Yes, TS-Parser, relax when you're fed svelte files
       },
diff --git a/frontend/viewer/lingui.config.ts b/frontend/viewer/lingui.config.ts
new file mode 100644
index 0000000000..3202a6f06d
--- /dev/null
+++ b/frontend/viewer/lingui.config.ts
@@ -0,0 +1,18 @@
+﻿import {jstsExtractor, svelteExtractor} from 'svelte-i18n-lingui/extractor';
+
+import {defineConfig} from '@lingui/cli';
+import {formatter} from '@lingui/format-json';
+
+export default defineConfig({
+  format: formatter({style: 'lingui'}),
+  locales: ['en', 'es', 'fr'],
+  sourceLocale: 'en',
+  catalogs: [
+    {
+      path: 'src/locales/{locale}',
+      include: ['src'],
+      exclude: ['src/locales']
+    },
+  ],
+  extractors: [jstsExtractor, svelteExtractor],
+});
diff --git a/frontend/viewer/package.json b/frontend/viewer/package.json
index e9dc8d3111..7cd8ea6c87 100644
--- a/frontend/viewer/package.json
+++ b/frontend/viewer/package.json
@@ -25,12 +25,17 @@
     "test:ui": "vitest --ui",
     "test:watch": "vitest",
     "check": "svelte-check --tsconfig ./tsconfig.json",
-    "lint": "eslint ."
+    "lint": "eslint .",
+    "i18n:extract": "lingui extract && lingui extract"
   },
   "devDependencies": {
     "@egoist/tailwindcss-icons": "^1.8.0",
     "@iconify-json/mdi": "^1.1.66",
+    "@lingui/cli": "^5.2.0",
+    "@lingui/format-json": "^5.2.0",
+    "@lingui/vite-plugin": "^5.2.0",
     "@mdi/js": "^7.4.47",
+    "@stylistic/eslint-plugin": "catalog:",
     "@sveltejs/vite-plugin-svelte": "catalog:",
     "@tailwindcss/typography": "^0.5.13",
     "@testing-library/jest-dom": "^6.6.3",
@@ -38,7 +43,6 @@
     "@testing-library/user-event": "^14.5.2",
     "@tsconfig/svelte": "^5.0.4",
     "@typescript-eslint/eslint-plugin": "catalog:",
-    "@stylistic/eslint-plugin": "catalog:",
     "@typescript-eslint/parser": "catalog:",
     "@vitest/ui": "catalog:",
     "eslint": "catalog:",
@@ -55,6 +59,7 @@
     "vitest": "catalog:"
   },
   "dependencies": {
+    "@lingui/core": "^5.2.0",
     "@microsoft/dotnet-js-interop": "^8.0.0",
     "@microsoft/signalr": "^8.0.0",
     "autoprefixer": "^10.4.19",
@@ -63,6 +68,7 @@
     "postcss": "catalog:",
     "svelte-dnd-action": "^0.9.57",
     "svelte-exmarkdown": "catalog:",
+    "svelte-i18n-lingui": "^0.2.2",
     "svelte-preprocess": "catalog:",
     "svelte-routing": "^2.12.0",
     "svelte-ux": "^0.76.0",
diff --git a/frontend/viewer/src/home/HomeView.svelte b/frontend/viewer/src/home/HomeView.svelte
index eca6b26a4f..6114a4c158 100644
--- a/frontend/viewer/src/home/HomeView.svelte
+++ b/frontend/viewer/src/home/HomeView.svelte
@@ -18,11 +18,14 @@
   import {
     useFwLiteConfig,
     useImportFwdataService,
-    useProjectsService, useTroubleshootingService
+    useProjectsService,
+    useTroubleshootingService,
   } from '$lib/services/service-provider';
   import AnchorListItem from '$lib/utils/AnchorListItem.svelte';
   import TroubleshootDialog from '$lib/troubleshoot/TroubleshootDialog.svelte';
   import ServersList from './ServersList.svelte';
+  import {t} from 'svelte-i18n-lingui';
+  import LocalizationPicker from '$lib/i18n/LocalizationPicker.svelte';
 
   const projectsService = useProjectsService();
   const importFwdataService = useImportFwdataService();
@@ -30,7 +33,8 @@
   const exampleProjectName = 'Example-Project';
 
   function dateTimeProjectSuffix(): string {
-    return new Date().toISOString()
+    return new Date()
+      .toISOString()
       .replace(/[^0-9]+/g, '-')
       .replace(/-$/, '');
   }
@@ -42,8 +46,7 @@
     try {
       createProjectLoading = true;
       let projectName = exampleProjectName;
-      if ($isDev)
-      {
+      if ($isDev) {
         if (customExampleProjectName) {
           projectName = customExampleProjectName;
         } else {
@@ -81,132 +84,141 @@
     }
   }
 
-
-  let projectsPromise = projectsService.localProjects().then(projects => projects.sort((p1, p2) => p1.name.localeCompare(p2.name)));
+  let projectsPromise = projectsService
+    .localProjects()
+    .then((projects) => projects.sort((p1, p2) => p1.name.localeCompare(p2.name)));
 
   async function refreshProjects() {
-    let promise = projectsService.localProjects().then(p => p.sort((p1, p2) => p1.name.localeCompare(p2.name)));
-    await promise;//avoids clearing out the list until the new list is fetched
+    let promise = projectsService.localProjects().then((p) => p.sort((p1, p2) => p1.name.localeCompare(p2.name)));
+    await promise; //avoids clearing out the list until the new list is fetched
     projectsPromise = promise;
   }
 
-
   const supportsTroubleshooting = useTroubleshootingService();
   let showTroubleshooting = false;
+
 </script>
-<AppBar title="Projects" class="bg-secondary min-h-12 shadow-md justify-between" menuIcon={null}>
+
+<AppBar title={$t`Dictionaries`} class="bg-secondary min-h-12 shadow-md justify-between" menuIcon={null}>
   <div slot="title" class="text-lg flex gap-2 items-center">
     <picture>
-      <source srcset={logoLight} media="(prefers-color-scheme: dark)">
-      <source srcset={logoDark} media="(prefers-color-scheme: light)">
-      <img src={logoDark} alt="Lexbox logo" class="h-6">
+      <source srcset={logoLight} media="(prefers-color-scheme: dark)" />
+      <source srcset={logoDark} media="(prefers-color-scheme: light)" />
+      <img src={logoDark} alt={$t`Lexbox logo`} class="h-6" />
     </picture>
-    <h3>Projects</h3>
+    <h3>{$t`Dictionaries`}</h3>
   </div>
   <div slot="actions" class="flex gap-2">
-    <Button
-      href={fwLiteConfig.feedbackUrl}
-      target="_blank"
-      size="sm"
-      variant="outline"
-      icon={mdiChatQuestion}>
-        Feedback
+    <Button href={fwLiteConfig.feedbackUrl} target="_blank" size="sm" variant="outline" icon={mdiChatQuestion}>
+      {$t`Feedback`}
     </Button>
     {#if supportsTroubleshooting}
-      <Button size="sm" variant="outline" icon={mdiFaceAgent} title="Troubleshoot" iconOnly={false}
-              on:click={() => showTroubleshooting = !showTroubleshooting}>
-      </Button>
-      <TroubleshootDialog bind:open={showTroubleshooting}/>
-    {/if}
-    <DevContent>
       <Button
-        href="/sandbox"
         size="sm"
         variant="outline"
-        icon={mdiTestTube}>
-        Sandbox
-      </Button>
+        icon={mdiFaceAgent}
+        title={$t`Troubleshoot`}
+        iconOnly={false}
+        on:click={() => (showTroubleshooting = !showTroubleshooting)}
+      ></Button>
+      <TroubleshootDialog bind:open={showTroubleshooting} />
+    {/if}
+    <DevContent>
+      <Button href="/sandbox" size="sm" variant="outline" icon={mdiTestTube}>Sandbox</Button>
     </DevContent>
+    <LocalizationPicker/>
   </div>
 </AppBar>
 <div class="mx-auto md:w-full md:py-4 max-w-2xl">
   <div class="flex-grow hidden md:block"></div>
   <div class="project-list">
     {#await projectsPromise}
-      <p>loading...</p>
+      <p>{$t`loading...`}</p>
     {:then projects}
       <div class="space-y-4 md:space-y-8">
         <div>
           <div class="flex flex-row">
-            <p class="sub-title">Local</p>
+            <p class="sub-title">{$t`Local`}</p>
             <div class="flex-grow"></div>
-            <Button icon={mdiRefresh}
-                    title="Refresh Projects"
-                    on:click={() => refreshProjects()}/>
+            <Button icon={mdiRefresh} title={$t`Refresh Projects`} on:click={() => refreshProjects()} />
           </div>
           <div>
-            {#each projects.filter(p => p.crdt) as project, i (project.id ?? i)}
+            {#each projects.filter((p) => p.crdt) as project, i (project.id ?? i)}
               {@const server = project.server}
               <AnchorListItem href={`/project/${project.name}`}>
-                <ListItem title={project.name}
-                          icon={mdiBookEditOutline}
-                          subheading={!server ? 'Local only' : ('Synced with ' + server.displayName)}
-                          loading={deletingProject === project.name}>
+                <ListItem
+                  title={project.name}
+                  icon={mdiBookEditOutline}
+                  subheading={!server ? $t`Local only` : $t`Synced with ${server.displayName}`}
+                  loading={deletingProject === project.name}
+                >
                   <div slot="actions">
                     {#if $isDev}
-                      <Button icon={mdiDelete} title="Delete" class="p-2" on:click={(e) => {
-                        e.preventDefault();
-                        void deleteProject(project.name);
-                      }} />
+                      <Button
+                        icon={mdiDelete}
+                        title={$t`Delete`}
+                        class="p-2"
+                        on:click={(e) => {
+                          e.preventDefault();
+                          void deleteProject(project.name);
+                        }}
+                      />
                     {/if}
-                    <Button icon={mdiChevronRight} class="p-2 pointer-events-none"/>
+                    <Button icon={mdiChevronRight} class="p-2 pointer-events-none" />
                   </div>
                 </ListItem>
               </AnchorListItem>
             {/each}
             <DevContent>
               <AnchorListItem href={`/testing/project-view`}>
-                <ListItem title="Test Project" icon={mdiTestTube}>
+                <ListItem title={$t`Test Project`} icon={mdiTestTube}>
                   <div slot="actions" class="pointer-events-none">
-                    <Button icon={mdiChevronRight} class="p-2"/>
+                    <Button icon={mdiChevronRight} class="p-2" />
                   </div>
                 </ListItem>
               </AnchorListItem>
             </DevContent>
-            {#if !projects.some(p => p.name === exampleProjectName) || $isDev}
-              <ListItem title="Create Example Project" on:click={() => createExampleProject()} loading={createProjectLoading}>
+            {#if !projects.some((p) => p.name === exampleProjectName) || $isDev}
+              <ListItem
+                title={$t`Create Example Project`}
+                on:click={() => createExampleProject()}
+                loading={createProjectLoading}
+              >
                 <div slot="actions" class="flex flex-nowrap gap-2">
                   {#if $isDev}
-                    <TextField bind:value={customExampleProjectName} placeholder="Project name..." on:click={(e) => e.stopPropagation()} />
+                    <TextField
+                      bind:value={customExampleProjectName}
+                      placeholder={$t`Project name...`}
+                      on:click={(e) => e.stopPropagation()}
+                    />
                   {/if}
-                  <Button icon={mdiBookPlusOutline} class="pointer-events-none p-2"/>
+                  <Button icon={mdiBookPlusOutline} class="pointer-events-none p-2" />
                 </div>
               </ListItem>
             {/if}
           </div>
         </div>
-        <ServersList localProjects={projects} {refreshProjects}/>
-        {#if projects.some(p => p.fwdata)}
+        <ServersList localProjects={projects} {refreshProjects} />
+        {#if projects.some((p) => p.fwdata)}
           <div>
-            <p class="sub-title">Classic FieldWorks Projects</p>
+            <p class="sub-title">{$t`Classic FieldWorks Projects`}</p>
             <div>
-              {#each projects.filter(p => p.fwdata) as project (project.id ?? project.name)}
+              {#each projects.filter((p) => p.fwdata) as project (project.id ?? project.name)}
                 <AnchorListItem href={`/fwdata/${project.name}`}>
                   <ListItem title={project.name}>
-                    <img slot="avatar" src={flexLogo} alt="FieldWorks logo" class="h-6"/>
+                    <img slot="avatar" src={flexLogo} alt={$t`FieldWorks logo`} class="h-6" />
                     <div slot="actions">
                       <DevContent invisible>
                         <Button
                           loading={importing === project.name}
                           icon={mdiBookArrowLeftOutline}
-                          title="Import"
+                          title={$t`Import`}
                           disabled={!!importing}
-                          on:click={async (e) =>
-                          {
+                          on:click={async (e) => {
                             e.preventDefault();
                             await importFwDataProject(project.name);
-                          }}>
-                        </Button>
+                          }}
+                        ></Button>
                       </DevContent>
                     </div>
                   </ListItem>
diff --git a/frontend/viewer/src/home/Server.svelte b/frontend/viewer/src/home/Server.svelte
index e07f3617db..18c7e84e67 100644
--- a/frontend/viewer/src/home/Server.svelte
+++ b/frontend/viewer/src/home/Server.svelte
@@ -7,6 +7,7 @@
   import {Button, ListItem, Settings} from 'svelte-ux';
   import AnchorListItem from '$lib/utils/AnchorListItem.svelte';
   import {useProjectsService} from '$lib/services/service-provider';
+  import {t} from 'svelte-i18n-lingui';
 
   const projectsService = useProjectsService();
 
@@ -44,7 +45,7 @@
   <div class="flex flex-row mb-2 items-end mr-2 md:mr-0">
     <div class="sub-title !my-0">
       {#if server}
-        {server.displayName} Server
+        {$t`${server.displayName} Server`}
       {:else}
         <div class="h-2 w-28 bg-surface-content/50 rounded-full animate-pulse"></div>
       {/if}
@@ -52,7 +53,7 @@
     <div class="flex-grow"></div>
     {#if status?.loggedIn}
       <Button icon={mdiRefresh}
-              title="Refresh Projects"
+              title={$t`Refresh Projects`}
               disabled={loading}
               on:click={() => dispatch('refreshProjects')}/>
       <LoginButton {status} on:status={() => dispatch('refreshAll')}/>
@@ -74,7 +75,7 @@
     {:else if !projects.length}
       <p class="text-surface-content/50 text-center elevation-1 md:rounded p-4">
         {#if status.loggedIn}
-          No projects
+          {$t`No projects`}
         {:else}
           <LoginButton {status} on:status={() => dispatch('refreshAll')}/>
         {/if}
@@ -89,7 +90,7 @@
                       loading={downloading === project.name}>
               <div slot="actions" class="pointer-events-none">
                 <Button disabled icon={mdiBookSyncOutline} class="p-2">
-                  Synced
+                  {$t`Synced`}
                 </Button>
               </div>
             </ListItem>
@@ -101,7 +102,7 @@
                     loading={downloading === project.name}>
             <div slot="actions" class="pointer-events-none">
               <Button icon={mdiBookArrowDownOutline} class="p-2">
-                Download
+                {$t`Download`}
               </Button>
             </div>
           </ListItem>
diff --git a/frontend/viewer/src/lib/auth/LoginButton.svelte b/frontend/viewer/src/lib/auth/LoginButton.svelte
index 98bdc8e96d..5758bd37ed 100644
--- a/frontend/viewer/src/lib/auth/LoginButton.svelte
+++ b/frontend/viewer/src/lib/auth/LoginButton.svelte
@@ -1,6 +1,7 @@
 ﻿<script context="module" lang="ts">
     import type {IAuthService, IServerStatus} from '$lib/dotnet-types';
     import {type Readable, writable, type Writable} from 'svelte/store';
+    import {t} from 'svelte-i18n-lingui';
 
     let shouldUseSystemWebViewStore: Writable<boolean> | undefined = undefined;
 
@@ -55,7 +56,7 @@
       <Button on:click={toggle} {loading} variant="fill" color="primary" icon={mdiAccountCircle}>
         {status.loggedInAs}
         <Menu {open} on:close={toggleOff} placement="bottom-end">
-          <MenuItem icon={mdiLogout} on:click={() => logout(server)}>Logout</MenuItem>
+          <MenuItem icon={mdiLogout} on:click={() => logout(server)}>{$t`Logout`}</MenuItem>
         </Menu>
       </Button>
     </Toggle>
@@ -66,7 +67,7 @@
                 color="primary"
                 on:click={() => login(server)}
                 icon={mdiLogin}>
-            Login to see projects
+          {$t`Login to see projects`}
         </Button>
     {:else}
         <Button {loading}
@@ -74,7 +75,7 @@
                 color="primary"
                 href="/api/auth/login/{server.id}"
                 icon={mdiLogin}>
-              Login to see projects
+          {$t`Login to see projects`}
         </Button>
     {/if}
 {/if}
diff --git a/frontend/viewer/src/lib/i18n/LocalizationPicker.svelte b/frontend/viewer/src/lib/i18n/LocalizationPicker.svelte
new file mode 100644
index 0000000000..b8e3f4e3d1
--- /dev/null
+++ b/frontend/viewer/src/lib/i18n/LocalizationPicker.svelte
@@ -0,0 +1,35 @@
+﻿<script lang="ts" module>
+  const hasSetLang = {value: false};
+</script>
+<script lang="ts">
+import {mdiTranslate} from '@mdi/js';
+import {MenuButton, type MenuOption} from 'svelte-ux';
+import {locale} from 'svelte-i18n-lingui';
+import {onMount} from 'svelte';
+
+const languages: MenuOption[] = [
+  {value: 'en', label: 'English'},
+  {value: 'fr', label: 'Français'},
+  {value: 'es', label: 'Español'},
+];
+async function setLanguage(lang: string) {
+  const wasDefault = lang === 'default';
+  if (!lang || wasDefault) lang = localStorage.getItem('locale') ?? 'en';
+  let {messages} = await import(`../../locales/${lang}.json?lingui`);
+  locale.set(lang, messages);
+  //only save when the user changes locale
+  if (!wasDefault) localStorage.setItem('locale', lang);
+}
+onMount(() => {
+  if (!hasSetLang.value) {
+    void setLanguage($locale);
+    hasSetLang.value = true;
+  }
+});
+</script>
+<MenuButton
+  options={languages}
+  value={$locale}
+  icon={mdiTranslate}
+  on:change={(e) => setLanguage(e.detail.value)}
+/>
diff --git a/frontend/viewer/src/locales/en.json b/frontend/viewer/src/locales/en.json
new file mode 100644
index 0000000000..cd283f74e8
--- /dev/null
+++ b/frontend/viewer/src/locales/en.json
@@ -0,0 +1,278 @@
+{
+  "dhBPPx": {
+    "message": "{0} Server",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        48
+      ]
+    ],
+    "translation": "{0} Server"
+  },
+  "Q01cKL": {
+    "message": "Classic FieldWorks Projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        204
+      ]
+    ],
+    "translation": "Classic FieldWorks Projects"
+  },
+  "0widty": {
+    "message": "Create Example Project",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        183
+      ]
+    ],
+    "translation": "Create Example Project"
+  },
+  "cnGeoo": {
+    "message": "Delete",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        159
+      ]
+    ],
+    "translation": "Delete"
+  },
+  "RY27RL": {
+    "message": "Dictionaries",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        102
+      ],
+      [
+        "src/home/HomeView.svelte",
+        109
+      ]
+    ],
+    "translation": "Dictionaries"
+  },
+  "mzI/c+": {
+    "message": "Download",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        105
+      ]
+    ],
+    "translation": "Download"
+  },
+  "YirHq7": {
+    "message": "Feedback",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        113
+      ]
+    ],
+    "translation": "Feedback"
+  },
+  "uKNYWn": {
+    "message": "FieldWorks logo",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        209
+      ]
+    ],
+    "translation": "FieldWorks logo"
+  },
+  "l3s5ri": {
+    "message": "Import",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        215
+      ]
+    ],
+    "translation": "Import"
+  },
+  "Cb8zzH": {
+    "message": "Lexbox logo",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        107
+      ]
+    ],
+    "translation": "Lexbox logo"
+  },
+  "+yD+Wu": {
+    "message": "loading...",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        136
+      ]
+    ],
+    "translation": "loading..."
+  },
+  "d5zxa4": {
+    "message": "Local",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        141
+      ]
+    ],
+    "translation": "Local"
+  },
+  "OgyJSr": {
+    "message": "Local only",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        152
+      ]
+    ],
+    "translation": "Local only"
+  },
+  "z7IcPF": {
+    "message": "Login to see projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/lib/auth/LoginButton.svelte",
+        70
+      ],
+      [
+        "src/lib/auth/LoginButton.svelte",
+        78
+      ]
+    ],
+    "translation": "Login to see projects"
+  },
+  "nOhz3x": {
+    "message": "Logout",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/lib/auth/LoginButton.svelte",
+        59
+      ]
+    ],
+    "translation": "Logout"
+  },
+  "BWz+2z": {
+    "message": "No projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        78
+      ]
+    ],
+    "translation": "No projects"
+  },
+  "Xp1CO/": {
+    "message": "Project name...",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        191
+      ]
+    ],
+    "translation": "Project name..."
+  },
+  "TG4hr2": {
+    "message": "Refresh Projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        56
+      ],
+      [
+        "src/home/HomeView.svelte",
+        143
+      ]
+    ],
+    "translation": "Refresh Projects"
+  },
+  "N2FcBE": {
+    "message": "Synced",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        93
+      ]
+    ],
+    "translation": "Synced"
+  },
+  "JXRN33": {
+    "message": "Synced with {0}",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        152
+      ]
+    ],
+    "translation": "Synced with {0}"
+  },
+  "/OCqal": {
+    "message": "Test Project",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        174
+      ]
+    ],
+    "translation": "Test Project"
+  },
+  "et+mIi": {
+    "message": "Troubleshoot",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        120
+      ]
+    ],
+    "translation": "Troubleshoot"
+  }
+}
\ No newline at end of file
diff --git a/frontend/viewer/src/locales/es.json b/frontend/viewer/src/locales/es.json
new file mode 100644
index 0000000000..ee83b974a4
--- /dev/null
+++ b/frontend/viewer/src/locales/es.json
@@ -0,0 +1,278 @@
+{
+  "dhBPPx": {
+    "message": "{0} Server",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        48
+      ]
+    ],
+    "translation": "{0} Servidor"
+  },
+  "Q01cKL": {
+    "message": "Classic FieldWorks Projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        204
+      ]
+    ],
+    "translation": "Proyectos Clásicos de FieldWorks"
+  },
+  "0widty": {
+    "message": "Create Example Project",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        183
+      ]
+    ],
+    "translation": "Crear Proyecto de Ejemplo"
+  },
+  "cnGeoo": {
+    "message": "Delete",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        159
+      ]
+    ],
+    "translation": "Eliminar"
+  },
+  "RY27RL": {
+    "message": "Dictionaries",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        102
+      ],
+      [
+        "src/home/HomeView.svelte",
+        109
+      ]
+    ],
+    "translation": "Diccionarios"
+  },
+  "mzI/c+": {
+    "message": "Download",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        105
+      ]
+    ],
+    "translation": "Descargar"
+  },
+  "YirHq7": {
+    "message": "Feedback",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        113
+      ]
+    ],
+    "translation": "Comentarios"
+  },
+  "uKNYWn": {
+    "message": "FieldWorks logo",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        209
+      ]
+    ],
+    "translation": "Logotipo de FieldWorks"
+  },
+  "l3s5ri": {
+    "message": "Import",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        215
+      ]
+    ],
+    "translation": "Importar"
+  },
+  "Cb8zzH": {
+    "message": "Lexbox logo",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        107
+      ]
+    ],
+    "translation": "Logotipo de Lexbox"
+  },
+  "+yD+Wu": {
+    "message": "loading...",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        136
+      ]
+    ],
+    "translation": "cargando..."
+  },
+  "d5zxa4": {
+    "message": "Local",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        141
+      ]
+    ],
+    "translation": "Local"
+  },
+  "OgyJSr": {
+    "message": "Local only",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        152
+      ]
+    ],
+    "translation": "Solo local"
+  },
+  "z7IcPF": {
+    "message": "Login to see projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/lib/auth/LoginButton.svelte",
+        70
+      ],
+      [
+        "src/lib/auth/LoginButton.svelte",
+        78
+      ]
+    ],
+    "translation": "Inicie sesión para ver proyectos"
+  },
+  "nOhz3x": {
+    "message": "Logout",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/lib/auth/LoginButton.svelte",
+        59
+      ]
+    ],
+    "translation": "Cerrar sesión"
+  },
+  "BWz+2z": {
+    "message": "No projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        78
+      ]
+    ],
+    "translation": "Ningún proyecto"
+  },
+  "Xp1CO/": {
+    "message": "Project name...",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        191
+      ]
+    ],
+    "translation": "Nombre del proyecto..."
+  },
+  "TG4hr2": {
+    "message": "Refresh Projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        56
+      ],
+      [
+        "src/home/HomeView.svelte",
+        143
+      ]
+    ],
+    "translation": "Actualizar Proyectos"
+  },
+  "N2FcBE": {
+    "message": "Synced",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        93
+      ]
+    ],
+    "translation": "Sincronizado"
+  },
+  "JXRN33": {
+    "message": "Synced with {0}",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        152
+      ]
+    ],
+    "translation": "Sincronizado con {0}"
+  },
+  "/OCqal": {
+    "message": "Test Project",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        174
+      ]
+    ],
+    "translation": "Proyecto de Prueba"
+  },
+  "et+mIi": {
+    "message": "Troubleshoot",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        120
+      ]
+    ],
+    "translation": "Solucionar problemas"
+  }
+}
diff --git a/frontend/viewer/src/locales/fr.json b/frontend/viewer/src/locales/fr.json
new file mode 100644
index 0000000000..e7fea2ed06
--- /dev/null
+++ b/frontend/viewer/src/locales/fr.json
@@ -0,0 +1,278 @@
+{
+  "dhBPPx": {
+    "message": "{0} Server",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        48
+      ]
+    ],
+    "translation": "{0} Serveur"
+  },
+  "Q01cKL": {
+    "message": "Classic FieldWorks Projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        204
+      ]
+    ],
+    "translation": "Projets Classiques de FieldWorks"
+  },
+  "0widty": {
+    "message": "Create Example Project",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        183
+      ]
+    ],
+    "translation": "Créer un Projet d'Exemple"
+  },
+  "cnGeoo": {
+    "message": "Delete",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        159
+      ]
+    ],
+    "translation": "Supprimer"
+  },
+  "RY27RL": {
+    "message": "Dictionaries",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        102
+      ],
+      [
+        "src/home/HomeView.svelte",
+        109
+      ]
+    ],
+    "translation": "Dictionnaires"
+  },
+  "mzI/c+": {
+    "message": "Download",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        105
+      ]
+    ],
+    "translation": "Télécharger"
+  },
+  "YirHq7": {
+    "message": "Feedback",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        113
+      ]
+    ],
+    "translation": "Retour d'information"
+  },
+  "uKNYWn": {
+    "message": "FieldWorks logo",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        209
+      ]
+    ],
+    "translation": "Logo FieldWorks"
+  },
+  "l3s5ri": {
+    "message": "Import",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        215
+      ]
+    ],
+    "translation": "Importer"
+  },
+  "Cb8zzH": {
+    "message": "Lexbox logo",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        107
+      ]
+    ],
+    "translation": "Logo Lexbox"
+  },
+  "+yD+Wu": {
+    "message": "loading...",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        136
+      ]
+    ],
+    "translation": "chargement..."
+  },
+  "d5zxa4": {
+    "message": "Local",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        141
+      ]
+    ],
+    "translation": "Local"
+  },
+  "OgyJSr": {
+    "message": "Local only",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        152
+      ]
+    ],
+    "translation": "Uniquement local"
+  },
+  "z7IcPF": {
+    "message": "Login to see projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/lib/auth/LoginButton.svelte",
+        70
+      ],
+      [
+        "src/lib/auth/LoginButton.svelte",
+        78
+      ]
+    ],
+    "translation": "Connectez-vous pour voir les projets"
+  },
+  "nOhz3x": {
+    "message": "Logout",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/lib/auth/LoginButton.svelte",
+        59
+      ]
+    ],
+    "translation": "Se déconnecter"
+  },
+  "BWz+2z": {
+    "message": "No projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        78
+      ]
+    ],
+    "translation": "Aucun projet"
+  },
+  "Xp1CO/": {
+    "message": "Project name...",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        191
+      ]
+    ],
+    "translation": "Nom du projet..."
+  },
+  "TG4hr2": {
+    "message": "Refresh Projects",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        56
+      ],
+      [
+        "src/home/HomeView.svelte",
+        143
+      ]
+    ],
+    "translation": "Actualiser les Projets"
+  },
+  "N2FcBE": {
+    "message": "Synced",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/Server.svelte",
+        93
+      ]
+    ],
+    "translation": "Synchronisé"
+  },
+  "JXRN33": {
+    "message": "Synced with {0}",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        152
+      ]
+    ],
+    "translation": "Synchronisé avec {0}"
+  },
+  "/OCqal": {
+    "message": "Test Project",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        174
+      ]
+    ],
+    "translation": "Projet de Test"
+  },
+  "et+mIi": {
+    "message": "Troubleshoot",
+    "placeholders": {},
+    "comments": [],
+    "origin": [
+      [
+        "src/home/HomeView.svelte",
+        120
+      ]
+    ],
+    "translation": "Résoudre les problèmes"
+  }
+}
diff --git a/frontend/viewer/vite.config.ts b/frontend/viewer/vite.config.ts
index 37fb3c3790..7f073963b5 100644
--- a/frontend/viewer/vite.config.ts
+++ b/frontend/viewer/vite.config.ts
@@ -1,6 +1,7 @@
 import {defineConfig} from 'vite';
 import {svelte} from '@sveltejs/vite-plugin-svelte';
 import {svelteTesting} from '@testing-library/svelte/vite';
+import {lingui} from '@lingui/vite-plugin';
 
 // https://vitejs.dev/config/
 export default defineConfig(({ mode, command }) => {
@@ -42,13 +43,17 @@ export default defineConfig(({ mode, command }) => {
     resolve: {
       alias: [{find: "$lib", replacement: "/src/lib"}]
     },
-    plugins: [svelte({
-      onwarn: (warning, handler) => {
-        // we don't have control over these warnings and there are lots
-        if (warning.filename?.includes('node_modules/svelte-ux')) return;
-        handler(warning);
-      },
-    }), svelteTesting()],
+    plugins: [
+      svelte({
+        onwarn: (warning, handler) => {
+          // we don't have control over these warnings and there are lots
+          if (warning.filename?.includes('node_modules/svelte-ux')) return;
+          handler(warning);
+        },
+      }),
+      svelteTesting(),
+      lingui()
+    ],
     server: {
       origin: 'http://localhost:5173',
       host: true,


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>frontend/viewer/src/home/Server.svelte (1)</summary><blockquote>

`78-78`: _🛠️ Refactor suggestion_

**Missing internationalization for "No projects" message**

The "No projects" text is still hardcoded. All user-facing text should be internationalized for consistency.

```diff
- No projects
+ {$t`No projects`}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (11)</summary><blockquote>

<details>
<summary>frontend/viewer/src/lib/i18n/LocalizationPicker.svelte (2)</summary><blockquote>

`11-14`: **Consider adding error handling to the dynamic import**

The current implementation doesn't handle potential errors when loading language files. If a file doesn't exist or there's a network issue, this could cause silent failures.


```diff
async function setLanguage(lang: string) {
-  let {messages} = await import(`../../locales/${lang}.json?lingui`);
-  locale.set(lang, messages);
+  try {
+    let {messages} = await import(`../../locales/${lang}.json?lingui`);
+    locale.set(lang, messages);
+  } catch (error) {
+    console.error(`Failed to load language file for ${lang}:`, error);
+    // Optionally: fallback to default language or show user notification
+  }
}
```

---

`6-10`: **Consider adding more languages and display names in native script**

For better user experience, consider showing language names in their native script, which helps users identify their language more easily.


```diff
const languages: MenuOption[] = [
-  {value: 'en', label: 'English'},
-  {value: 'fr', label: 'Français'},
-  {value: 'es', label: 'Español'},
+  {value: 'en', label: 'English'},
+  {value: 'fr', label: 'Français'},
+  {value: 'es', label: 'Español'},
+  // Examples of other languages you might want to add in the future
+  // {value: 'de', label: 'Deutsch'},
+  // {value: 'ja', label: '日本語'},
+  // {value: 'zh', label: '中文'},
];
```

</blockquote></details>
<details>
<summary>frontend/viewer/src/lib/auth/LoginButton.svelte (1)</summary><blockquote>

`57-57`: **Consider internationalizing the user's name display**

The logged-in user's name display is not internationalized. While the name itself shouldn't be translated, you might want to consider adding a translatable format string for how the name is presented.


```diff
<Button on:click={toggle} {loading} variant="fill" color="primary" icon={mdiAccountCircle}>
-  {status.loggedInAs}
+  {$t`Logged in as: {0}`(status.loggedInAs)}
  <Menu {open} on:close={toggleOff} placement="bottom-end">
```

This would allow translators to adjust how the username is displayed according to their language's conventions.

</blockquote></details>
<details>
<summary>frontend/viewer/src/home/Server.svelte (2)</summary><blockquote>

`93-94`: **Consider internationalizing the "Synced" text**

While you've internationalized many strings, the "Synced" text on line 94 is still hardcoded. For consistency, this should also use the translation function.

```diff
- <Button disabled icon={mdiBookSyncOutline} class="p-2">
-   Synced
- </Button>
+ <Button disabled icon={mdiBookSyncOutline} class="p-2">
+   {$t`Synced`}
+ </Button>
```

---

`104-106`: **Consider internationalizing the "Download" button text**

The "Download" text on line 105 is still hardcoded. For consistency with the rest of the internationalization effort, this should also use the translation function.

```diff
- <Button icon={mdiBookArrowDownOutline} class="p-2">
-   Download
- </Button>
+ <Button icon={mdiBookArrowDownOutline} class="p-2">
+   {$t`Download`}
+ </Button>
```

</blockquote></details>
<details>
<summary>frontend/viewer/src/locales/fr.json (1)</summary><blockquote>

`12-12`: **Ensure consistent placement of spaces in translations**

In French, there should be a space before the colon. In the translation "{0} Serveur", consider whether a space is needed between the variable and the word "Serveur", depending on how the variable will be rendered.

</blockquote></details>
<details>
<summary>frontend/viewer/README.md (4)</summary><blockquote>

`5-6`: **Format URLs properly in Markdown**

The URLs are presented as bare URLs. For better Markdown formatting, consider using proper link syntax.

```diff
- We're using a different library here from the lexbox UI. The core lib is https://lingui.dev/
- with a Svelte plugin https://github.com/HenryLie/svelte-i18n-lingui
+ We're using a different library here from the lexbox UI. The core lib is [Lingui](https://lingui.dev/)
+ with a Svelte plugin [svelte-i18n-lingui](https://github.com/HenryLie/svelte-i18n-lingui)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

5-5: Bare URL used
null

(MD034, no-bare-urls)

---

6-6: Bare URL used
null

(MD034, no-bare-urls)

</details>

</details>

---

`17-17`: **Improve readability with proper punctuation**

The sentence could benefit from a comma to improve readability.

```diff
- if you want you can then feed those files to an AI and it'll translate them for you.
+ if you want, you can then feed those files to an AI and it'll translate them for you.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~17-~17: A comma might be missing here.
Context: ...te the files under /src/locales/ if you want you can then feed those files to an AI ...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

---

[uncategorized] ~17-~17: Use a comma before ‘and’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...t you can then feed those files to an AI and it'll translate them for you.  #### adv...

(COMMA_COMPOUND_SENTENCE)

</details>

</details>

---

`3-24`: **Good i18n documentation, but consider adding more details**

The documentation provides clear and concise instructions for using i18n, but could benefit from some additional information:
1. A brief explanation of how to add new languages
2. Information about the supported locales
3. How to switch between languages in the application

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~17-~17: A comma might be missing here.
Context: ...te the files under /src/locales/ if you want you can then feed those files to an AI ...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

---

[uncategorized] ~17-~17: Use a comma before ‘and’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...t you can then feed those files to an AI and it'll translate them for you.  #### adv...

(COMMA_COMPOUND_SENTENCE)

---

[uncategorized] ~21-~21: Possible missing comma found.
Context: ...ou.  #### advanced usage  for formatted values you can do this: ```sveltehtml <span>{$...

(AI_HYDRA_LEO_MISSING_COMMA)

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

5-5: Bare URL used
null

(MD034, no-bare-urls)

---

6-6: Bare URL used
null

(MD034, no-bare-urls)

</details>

</details>

---

`21-21`: **Add a comma for better readability**

Add a comma after "for formatted values" to improve readability.

```diff
- for formatted values you can do this:
+ for formatted values, you can do this:
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~21-~21: Possible missing comma found.
Context: ...ou.  #### advanced usage  for formatted values you can do this: ```sveltehtml <span>{$...

(AI_HYDRA_LEO_MISSING_COMMA)

</details>

</details>

</blockquote></details>
<details>
<summary>frontend/viewer/src/home/HomeView.svelte (1)</summary><blockquote>

`27-28`: **Consider adding aria-label for better accessibility**

While internationalizing the UI, consider also adding appropriate aria-labels to buttons that use icon-only displays (like the Troubleshoot button) to improve accessibility for screen readers.

```diff
<Button
  size="sm"
  variant="outline"
  icon={mdiFaceAgent}
  title={$t`Troubleshoot`}
  iconOnly={false}
+ aria-label={$t`Troubleshoot`}
  on:click={() => (showTroubleshooting = !showTroubleshooting)}
></Button>
```


Also applies to: 102-129

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 778d0a9ad7da249d49af9a6327b51723c559522d and b574f73154e628804cea6ffaf50d95c251a6dc15.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `frontend/pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (11)</summary>

* `frontend/viewer/README.md` (1 hunks)
* `frontend/viewer/lingui.config.ts` (1 hunks)
* `frontend/viewer/package.json` (3 hunks)
* `frontend/viewer/src/home/HomeView.svelte` (3 hunks)
* `frontend/viewer/src/home/Server.svelte` (2 hunks)
* `frontend/viewer/src/lib/auth/LoginButton.svelte` (3 hunks)
* `frontend/viewer/src/lib/i18n/LocalizationPicker.svelte` (1 hunks)
* `frontend/viewer/src/locales/en.json` (1 hunks)
* `frontend/viewer/src/locales/es.json` (1 hunks)
* `frontend/viewer/src/locales/fr.json` (1 hunks)
* `frontend/viewer/vite.config.ts` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* frontend/viewer/lingui.config.ts
* frontend/viewer/src/locales/en.json

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>frontend/viewer/README.md</summary>

[uncategorized] ~17-~17: A comma might be missing here.
Context: ...te the files under /src/locales/ if you want you can then feed those files to an AI ...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

---

[uncategorized] ~17-~17: Use a comma before ‘and’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...t you can then feed those files to an AI and it'll translate them for you.  #### adv...

(COMMA_COMPOUND_SENTENCE)

---

[uncategorized] ~21-~21: Possible missing comma found.
Context: ...ou.  #### advanced usage  for formatted values you can do this: ```sveltehtml <span>{$...

(AI_HYDRA_LEO_MISSING_COMMA)

</details>

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>frontend/viewer/README.md</summary>

5-5: Bare URL used
null

(MD034, no-bare-urls)

---

6-6: Bare URL used
null

(MD034, no-bare-urls)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: Build API / publish-api
* GitHub Check: Build UI / publish-ui
* GitHub Check: check-and-lint
* GitHub Check: Build FW Lite and run tests
* GitHub Check: Analyze (csharp)

</details>

<details>
<summary>🔇 Additional comments (25)</summary><blockquote>

<details>
<summary>frontend/viewer/vite.config.ts (2)</summary>

`46-56`: **The code looks good - plugin integration is properly implemented**

The lingui plugin has been correctly added to the Vite configuration's plugin array, which will enable internationalization support during the build process.

---

`4-4`: <details>
<summary>❓ Verification inconclusive</summary>

**Consider configuring the lingui plugin**

The lingui plugin has been added without any configuration options. While the default settings may work, you might want to consider customizing it based on your project's needs.





Also applies to: 55-55

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there's a lingui.config.ts file that might contain config that's being used
fd -t f -e ts -e js "lingui.config" --exec cat {}
```

Length of output: 517

---



</details>

**Action Needed: Confirm Automatic Usage of Lingui Configuration**

I noticed that the Vite Lingui plugin is imported without inline configuration in `frontend/viewer/vite.config.ts` (line 4) and similarly at line 55. However, a comprehensive `lingui.config.ts` exists in the repository, which sets options for locales, formatter, catalogs, and extractors. Please verify that the Vite plugin automatically loads and applies the configuration from `lingui.config.ts`. If it does not, consider explicitly passing the desired configuration options in the plugin invocation.

</details>
<details>
<summary>frontend/viewer/src/lib/auth/LoginButton.svelte (3)</summary>

`4-4`: **Import looks good - translation function properly imported**

The translation function is imported correctly from the svelte-i18n-lingui library.

---

`59-59`: **Properly implemented translation for Logout button**

The static text has been correctly replaced with the translatable version using the `$t` template literal tag.

---

`70-70`: **Consistent translation implementation across button instances**

Both instances of the "Login to see projects" text have been properly internationalized in a consistent manner.




Also applies to: 78-78

</details>
<details>
<summary>frontend/viewer/src/locales/es.json (3)</summary>

`1-242`: **Spanish translations look good, but verify with native speakers**

The Spanish translations appear to be well-structured and comprehensive. The JSON format follows the Lingui standard with message IDs, original messages, origins, and translations.



While the translations seem reasonable, it's always best to have them reviewed by native Spanish speakers, especially for technical terms or industry-specific vocabulary. Consider having a native speaker review phrases like "Crear Proyecto de Ejemplo" to ensure they convey the intended meaning in the application context.

---

`1-242`: <details>
<summary>❓ Verification inconclusive</summary>

**Consider adding placeholders for variable text elements**

For messages that might include variable text (like server names, usernames, etc.), ensure you're using placeholders consistently. I noticed that some messages like "Synced with {0}" already use placeholders, which is good practice.


For testing consistency in placeholder usage across all locale files:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check all JSON files in the locales directory for messages with placeholders
fd -t f -e json . src/locales --exec sh -c 'echo "File: {}"; cat {} | jq \'to_entries[] | select(.value.message | contains("{") and contains("}")) | {id: .key, message: .value.message}\''
```

Length of output: 370

---



</details>

**Placeholder Consistency Verification**

The JSON locale file shows that some messages (like `"Synced with {0}"`) correctly use placeholders. However, our automated verification script encountered syntax issues, so the consistency across all locale files couldn’t be confirmed automatically. Please manually verify that any messages incorporating variable content (e.g., server names, project names, usernames) consistently use placeholders.

- Verify messages in all locale JSON files (e.g., under `src/locales/`) to ensure dynamic text is not hard-coded.
- Re-run or adjust the placeholder verification command if needed.

---

`126-137`: <details>
<summary>✅ Verification successful</summary>

**Consider translating "Local"**

The term "Local" is currently the same in both English and Spanish. While this might be valid, you might want to consider if "Local" has a more appropriate Spanish translation in the context of your application, such as "Local" with proper Spanish pronunciation or "Área local".

---

🌐 Web query:

```web
Is "Local" the correct Spanish translation for "Local" when referring to local storage or local environment?
```

💡 Result:

The Spanish translation of "local" when referring to concepts like **local storage** or **local environment** depends on the context:  

1. **General Use**:  
   - The direct translation of "local" as an adjective is **"local"** (e.g., *la población local* for "local population")[1][4][10].  
   - Example:  
     > *El gobierno local aprobó nuevas regulaciones.* ("The local government approved new regulations.")[1][4]  

2. **Technical/Computing Contexts**:  
   - **"Local environment"** is often translated as **"entorno local"** (e.g., *entorno local de desarrollo* for "local development environment")[2][8].  
   - **"Local storage"** typically uses **"almacenamiento local"** (e.g., *Los datos se guardan en el almacenamiento local del dispositivo*)[5][7].  

3. **Nuances**:  
   - In phrases like "local environment," **"entorno local"** emphasizes the immediate or surrounding context[2][8].  
   - For programming or system-related terms, **"local"** remains valid but is often part of compound phrases (e.g., *variables locales* for "local variables")[5][7].  

In summary:  
- **Correct Translation**: *local* is generally acceptable.  
- **Specific Technical Terms**: Use *entorno local* (environment) or *almacenamiento local* (storage) for clarity[2][5][8].

Citations:

- 1: https://dictionary.cambridge.org/us/dictionary/english-spanish/local
- 2: https://www.spanishdict.com/translate/el%20entorno
- 3: https://deh.acgov.org/operations/home-based-food-business.page
- 4: https://www.collinsdictionary.com/dictionary/english-spanish/local
- 5: https://www.pyopensci.org/python-package-guide/TRANSLATING.html
- 6: https://www.dhses.ny.gov/state-emergency-response-commission-serc
- 7: https://www.spanishdict.com/translate/locales
- 8: https://context.reverso.net/translation/spanish-english/el+entorno+local
- 9: https://acf.gov/orr/policy-guidance/unaccompanied-children-program-policy-guide-section-5
- 10: https://www.spanishdict.com/translate/local

---



</details>

**Review Update: Translation Confirmation for "Local"**

Based on our verification, the Spanish term "Local" is acceptable in general contexts—including technical ones such as local storage or environment—unless your application demands a more descriptive term. In this case:

- **File:** `frontend/viewer/src/locales/es.json` (Lines 126-137)
- **Verification:** The web query indicates that using "Local" is correct for most contexts. Only if a more technical or specific nuance is needed (e.g., "entorno local" or "almacenamiento local") should you consider a change.

No changes are required at this time.

</details>
<details>
<summary>frontend/viewer/src/home/Server.svelte (3)</summary>

`10-10`: **Added i18n support correctly with svelte-i18n-lingui**

The import of the translation function from the Lingui library is properly implemented, matching the internationalization strategy described in the PR.

---

`48-48`: **Successfully implemented translation for server display name**

The string template correctly implements the i18n functionality for the server display name, allowing for dynamic content within the translated string.

---

`56-56`: **Correctly added translation for button title**

The "Refresh Projects" button title is now properly internationalized using the translation function.

</details>
<details>
<summary>frontend/viewer/src/locales/fr.json (2)</summary>

`1-242`: **French translations look good overall**

The French translations are appropriately structured and follow the Lingui JSON format correctly. The translations appear to be appropriate for the given English phrases.

---

`136-136`: <details>
<summary>✅ Verification successful</summary>

**Consider translating "Local" to French**

The word "Local" (line 136) has been kept the same in both English and French. While this may be acceptable as the word is similar in both languages, consider using a more idiomatic French translation if appropriate.

---

🌐 Web query:

```web
What is the proper French translation for "Local" in a software interface context?
```

💡 Result:

The proper French translation for "Local" in a software interface context depends on the specific usage, but **"Local"** is commonly used as a direct translation for technical terms like "local settings" or "local storage." However, nuances exist:

1. **General Interface Use**:  
   - For buttons, menus, or labels (e.g., "Local Settings"), **"Local"** is often retained in French software interfaces, especially in technical contexts[1][4].
   - Example: *Paramètres locaux* (Local Settings).

2. **Regional Adaptation**:  
   - In Canadian French, minor adjustments might apply (e.g., formatting or terminology preferences), but "Local" remains standard[9].

3. **Context-Specific Cases**:  
   - For phrases like "local file" or "local network," use **"local"** (e.g., *Fichier local* or *Réseau local*)[10].
   - Avoid literal translations like *régional* or *proche*, which imply geographic proximity rather than technical scope[6].

For consistency, collaborate with localization experts to create a **terminology glossary** (e.g., defining "Local" as *Local* in all contexts) and validate translations through linguistic testing[1][10]. Tools like SimpleLocalize or Lokalise can streamline this process by integrating glossaries directly into the localization workflow[2][5].

Citations:

- 1: https://www.atltranslate.com/blog/software-localization-process
- 2: https://simplelocalize.io
- 3: https://phrase.com/blog/posts/software-localization/
- 4: https://www.stepes.com/french-translation-services/
- 5: https://lokalise.com
- 6: https://edutechwiki.unige.ch/en/Software_localization
- 7: https://github.com/LibreTranslate/LibreTranslate
- 8: https://crowdin.com/software-localization
- 9: https://www.aspena.com/application-and-software-translation
- 10: https://www.csoftintl.com/software-gui-localization/

---



</details>

**Translation is appropriate as-is**

After verifying with localization references, it turns out that using “Local” in the French interface is acceptable for technical contexts. The term is commonly kept as “Local” when referring to technical features such as local files, local settings, or local storage. No changes are needed here unless the context specifically requires a more descriptive phrase (e.g., “Paramètres locaux” for settings).

</details>
<details>
<summary>frontend/viewer/package.json (4)</summary>

`29-29`: **Successfully added i18n extraction script**

The new script for extracting localization strings matches the instructions provided in the README.

---

`34-36`: **Appropriate Lingui CLI and formatting dependencies added**

The added devDependencies for Lingui tools (CLI, JSON formatter, and Vite plugin) are correctly specified with compatible versioning.

---

`62-62`: **Core i18n dependencies correctly added**

The `@lingui/core` and `svelte-i18n-lingui` dependencies are correctly added to support the internationalization implementation.



Also applies to: 71-71

---

`29-29`: <details>
<summary>✅ Verification successful</summary>

**Consider adding a command to compile translations**

Currently, you've added the command to extract strings for translation, but there's typically also a command to compile translations for use in the application.

```diff
  "scripts": {
    "dev-app": "vite",
    "dev": "vite build -m web-component --watch",
    "lexbox-dev": "vite build -m web-component",
    "build": "vite build -m web-component",
    "build-app": "vite build",
    "preview": "vite preview",
    "test": "vitest run",
    "test:ui": "vitest --ui",
    "test:watch": "vitest",
    "check": "svelte-check --tsconfig ./tsconfig.json",
    "lint": "eslint .",
    "i18n:extract": "lingui extract"
+   "i18n:compile": "lingui compile"
  },
```

---

🌐 Web query:

```web
Does Lingui require a compile step after extraction for svelte-i18n-lingui?
```

💡 Result:

Yes, Lingui requires a compile step after message extraction when using **svelte-i18n-lingui**. Here's the workflow and rationale:

---

### Key Workflow Steps
1. **Extraction** (`lingui extract`):  
   Identifies translatable strings in your codebase and generates `.po`/`.json` message catalogs [2][4][6].  
   Example output: `src/locales/en/messages.po`

2. **Compilation** (`lingui compile`):  
   Converts human-readable catalogs into optimized runtime files (`.js`/`.ts`) for production use [1][2][4].  
   Example output: `src/locales/en/messages.js`

---

### Why Compilation Matters
- **Reduced Bundle Size**: Compilation replaces full message texts with short hashes in production builds, minimizing payload [1][4][10].
- **Runtime Efficiency**: Compiled catalogs use `JSON.parse()` for faster initialization [2][4].
- **Framework Integration**: `svelte-i18n-lingui` relies on compiled catalogs to provide reactive translations via Svelte stores [1][7].

---

### Implementation Example
**package.json** setup:
```json
{
  "scripts": {
    "extract": "lingui extract",
    "compile": "lingui compile --typescript"
  }
}
```
Run:
```bash
npm run extract  # Generate/update message catalogs
npm run compile  # Create production-ready runtime files
```

---

### Critical Notes
- Without compilation, translations won't appear in production builds – you'll see message IDs instead of localized text [10].
- The compile step is framework-agnostic and required for all Lingui integrations (React, Svelte, etc.) [4][6][8].
- CI/CD pipelines should include both `extract` and `compile` commands during build processes [2][6].

For Svelte-specific configurations, refer to the `svelte-i18n-lingui` documentation on configuring [`lingui.config.ts`](https://github.com/HenryLie/svelte-i18n-lingui#configure-lingui) [1][6].

Citations:

- 1: https://github.com/HenryLie/svelte-i18n-lingui
- 2: https://lingui.dev/ref/cli
- 3: https://caisy.io/blog/sveltekit-i18n-guide
- 4: https://lingui.dev/tutorials/react
- 5: https://centus.com/blog/svelte-localization
- 6: https://lingui.dev/ref/conf
- 7: https://www.npmjs.com/package/svelte-i18n
- 8: https://lingui.dev/blog/2024/11/28/announcing-lingui-5.0
- 9: https://crowdin.com/blog/2024/03/12/svelte-localization
- 10: https://github.com/lingui/js-lingui/discussions/1942

---



</details>

**Add the Lingui compile command to package.json**

The web query confirms that for projects using svelte-i18n-lingui, a compile step is indeed required to convert the extracted messages into optimized runtime files. Please add the compile script as shown below to ensure that production builds include properly compiled translations.

- In `frontend/viewer/package.json`, include the following diff snippet:
  ```diff
    "i18n:extract": "lingui extract"
  + "i18n:compile": "lingui compile"
  ```
  
This change will prevent issues where untranslated message IDs appear in production.

</details>
<details>
<summary>frontend/viewer/src/home/HomeView.svelte (8)</summary>

`27-28`: **Internationalization support added correctly**

Good addition of the necessary i18n imports - `t` from svelte-i18n-lingui and LocalizationPicker component. This aligns with the PR objectives to implement internationalization throughout the Svelte frontend.

---

`35-40`: **Date formatting has been improved**

The dateTimeProjectSuffix function has been reformatted for better readability while maintaining the same functionality.

---

`87-89`: **Promise handling improved for better readability**

The projectsPromise initialization has been restructured with cleaner formatting, making the code more readable while preserving the functionality.

---

`91-95`: **Improved user experience during project refresh**

The refreshProjects function now explicitly awaits the promise before assignment, which prevents the UI from clearing existing projects until new data is fetched. This eliminates potential UI flashes and provides a better user experience.

---

`102-102`: **All static text successfully internationalized**

Excellent implementation of internationalization across the component. All user-facing text strings have been wrapped with the `$t` template literal, making them available for translation.



Also applies to: 107-107, 109-109, 113-113, 120-120, 136-136, 141-141, 143-143, 152-152, 159-159, 174-174, 183-183, 191-191, 204-204, 209-209, 215-215

---

`129-129`: **Added language switching capability**

The LocalizationPicker component has been properly integrated in the AppBar actions section, providing users with a way to switch between available languages.

---

`201-202`: **ServersList component passed projects correctly**

The ServersList component is correctly passed the local projects and refresh function, maintaining the existing functionality while adapting to the code formatting changes.

---

`27-28`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify right-to-left (RTL) language support**

Since you're implementing internationalization, check if your UI layout properly supports right-to-left languages like Arabic or Hebrew, especially for elements with directional content like the ListItems with actions on the right side.




Also applies to: 102-233

---

🏁 Script executed:

```shell
#!/bin/bash
# Look for RTL support in the codebase
echo "Checking for RTL support in CSS or configuration..."
rg -i "rtl|direction|right-to-left" --type css
rg -i "rtl|direction|right-to-left" --type ts --type svelte
```

Length of output: 65787

---



</details>

**RTL Support Verification: Manual Confirmation Required**

The automated search didn’t conclusively identify RTL-specific code in your Svelte files (partly due to ripgrep not recognizing the svelte file type). Although the Bootstrap CSS defines several directional properties, there isn’t a clear implementation of RTL adjustments in your Svelte components—especially for the UI elements (e.g., ListItems with right-aligned actions) referenced in lines 102–233.

- Ensure that your UI layout conditionally applies RTL classes or styles when internationalizing for RTL languages.
- Verify that components like the LocalizationPicker and any listed action items adjust correctly when switching to RTL languages.
- Consider updating the search command (or verifying manually) using a file glob (e.g., `rg -i 'rtl|right-to-left' -g '*.svelte'`) to double-check for potential RTL implementations in Svelte files.

Please perform a manual review of these UI sections to confirm proper RTL support.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T06:10:55Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>frontend/viewer/src/locales/en.json (1)</summary><blockquote>

`66-77`: **Key Ordering Consistency**

There is a minor inconsistency in the ordering of keys among the entries (for example, the entry with key `"mzI/c+"` lists `"translation"` before `"message"` while most entries follow `"message"` then `"translation"`). Although JSON does not enforce key order, maintaining a consistent order (e.g., `"message"`, `"translation"`, `"placeholders"`, `"comments"`, `"origin"`) can improve readability and ease future maintenance.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b574f73154e628804cea6ffaf50d95c251a6dc15 and 4c018f23d568e1598bcc2e3a60302d3d8dd0c493.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `frontend/viewer/eslint.config.js` (1 hunks)
* `frontend/viewer/lingui.config.ts` (1 hunks)
* `frontend/viewer/src/home/Server.svelte` (5 hunks)
* `frontend/viewer/src/locales/en.json` (1 hunks)
* `frontend/viewer/src/locales/es.json` (1 hunks)
* `frontend/viewer/src/locales/fr.json` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (4)</summary>

* frontend/viewer/src/locales/es.json
* frontend/viewer/lingui.config.ts
* frontend/viewer/src/locales/fr.json
* frontend/viewer/src/home/Server.svelte

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Build API / publish-api
* GitHub Check: Build UI / publish-ui
* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Build FW Lite and run tests
* GitHub Check: Analyze (csharp)
* GitHub Check: check-and-lint

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>frontend/viewer/eslint.config.js (2)</summary>

`120-122`: **Proper configuration for Lingui TypeScript support**

This change properly configures the TypeScript parser to recognize the Lingui configuration file, which is essential for the internationalization implementation. The `allowDefaultProject` setting ensures ESLint can correctly analyze the Lingui TypeScript configuration without throwing parser errors.

---

`108-113`: **Good import restriction to enforce i18n consistency**

The ESLint rule to restrict imports from `svelte-intl-precompile` and direct developers to use `$lib/i18n` instead ensures consistency in the i18n implementation across the codebase. This aligns well with the PR objective of implementing internationalization using Lingui.

</details>
<details>
<summary>frontend/viewer/src/locales/en.json (1)</summary>

`1-278`: **Overall File Structure and Content Verification**

This new localization JSON file is well-structured and comprehensive. It clearly defines key-value pairs for each UI message with detailed metadata (including message, translation, placeholders, comments, and origin). The inclusion of the `"origin"` field for traceability enhances maintainability and eases future updates. Overall, the file aligns with the i18n feature requirements as outlined in the PR objectives.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T15:03:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>frontend/viewer/src/locales/fr.json (1)</summary><blockquote>

`279-279`: _⚠️ Potential issue_

**Remove Stray Line Causing Invalid JSON**

There is an unexpected extra line (containing the text `279`) at the end of the file. This stray line is not valid within a JSON object and can lead to parsing errors. Please remove this line to ensure the file remains valid JSON.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>frontend/viewer/src/locales/fr.json (1)</summary><blockquote>

`1-278`: **Overall JSON Structure is Well-Formed**

The JSON object correctly encapsulates all translation entries. Each entry follows the expected schema with `"message"`, `"placeholders"`, `"comments"`, `"origin"`, and `"translation"`. The translations are clear, and the placeholders (e.g., `{0}`) are properly mirrored in the translation. 

A minor nitpick: Some keys (like `"mzI/c+"`) include special characters. If these keys are auto-generated by your i18n tool and used internally, this is acceptable. However, if you intend to maintain or reference them manually, consider using more descriptive or simplified IDs for better maintainability.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4c018f23d568e1598bcc2e3a60302d3d8dd0c493 and 8290146408f6e2bacbb3d7792eeeda264388fecd.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `frontend/viewer/eslint.config.js` (1 hunks)
* `frontend/viewer/lingui.config.ts` (1 hunks)
* `frontend/viewer/src/home/Server.svelte` (5 hunks)
* `frontend/viewer/src/locales/en.json` (1 hunks)
* `frontend/viewer/src/locales/es.json` (1 hunks)
* `frontend/viewer/src/locales/fr.json` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* frontend/viewer/lingui.config.ts
* frontend/viewer/src/locales/es.json
* frontend/viewer/eslint.config.js
* frontend/viewer/src/locales/en.json
* frontend/viewer/src/home/Server.svelte

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Build API / publish-api
* GitHub Check: Build UI / publish-ui
* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: check-and-lint
* GitHub Check: Build FW Lite and run tests
* GitHub Check: Analyze (csharp)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T15:24:17Z
user: login: myieye
body: Looks good!!

I love how much less effort it is to add translations. The biggest win is not having to think about where to put each translation.

What do you think of a CI check for forgotten texts?
We could e.g. do this:
```
lingui extract
git diff --exit-code --quiet src/locales/
```

That would also pick up the awkward way that you need to run `extract` twice before the json files are really "settled" and prevent unnecessary diffs in later PRs.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T15:28:23Z
user: login: hahn-kev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T02:53:45Z
user: login: hahn-kev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T02:54:31Z
user: login: hahn-kev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T02:55:47Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>frontend/viewer/package.json (1)</summary><blockquote>

`28-29`: **Consider a script for initializing new languages**

The `i18n:extract` script is correctly added for extracting translatable strings. You might want to consider adding a complementary script for scaffolding new language files to streamline the process of adding support for additional languages.

```diff
  "scripts": {
    "lint": "eslint .",
    "i18n:extract": "lingui extract && lingui extract"
+   "i18n:add-language": "lingui extract --locale"
  },
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8290146408f6e2bacbb3d7792eeeda264388fecd and 25080d5ab6402e28316272952a303bf9c1082720.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `frontend/viewer/package.json` (3 hunks)
* `frontend/viewer/src/home/HomeView.svelte` (3 hunks)
* `frontend/viewer/src/lib/i18n/LocalizationPicker.svelte` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* frontend/viewer/src/home/HomeView.svelte

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Build UI / publish-ui
* GitHub Check: Build API / publish-api
* GitHub Check: check-and-lint
* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Build FW Lite and run tests
* GitHub Check: Analyze (csharp)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>frontend/viewer/src/lib/i18n/LocalizationPicker.svelte (4)</summary>

`1-3`: **Module script usage is appropriate here**

The module script properly defines a shared variable to track language initialization status across all instances of this component.

---

`15-22`: **The language setting logic is well implemented**

The `setLanguage` function handles different scenarios effectively:
- Falls back to localStorage value or default 'en' when needed
- Dynamically imports language files
- Updates the locale
- Persists user preferences in localStorage only when explicitly changed by the user

This approach is robust and follows best practices for handling user language preferences.

---

`23-28`: **Good initialization pattern with safeguards**

The `onMount` with the flag check prevents multiple initializations, which is a good practice. The use of `void` to handle the Promise without awaiting it is appropriate since we don't need to wait for completion in the mount phase.

---

`30-35`: **Properly addressed previous review feedback**

The MenuButton implementation correctly uses `value={$locale}` to display the current language, addressing the previous review feedback.

</details>
<details>
<summary>frontend/viewer/package.json (3)</summary>

`34-36`: **Lingui dependencies match the best practices**

The Lingui toolchain dependencies are correctly configured with the appropriate semver ranges, matching the recommended setup for Lingui v5.

---

`62-62`: **Core i18n dependencies are properly configured**

The `@lingui/core` and `svelte-i18n-lingui` packages are correctly added as runtime dependencies since they're needed at runtime, while build tools are in devDependencies.



Also applies to: 71-71

---

`38-38`: <details>
<summary>❓ Verification inconclusive</summary>

**Unrelated ESLint plugin modification**

This change to the `@stylistic/eslint-plugin` appears unrelated to the i18n implementation. Ensure this was an intentional change and not accidental.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check the git history to see if this was a separate change or part of the i18n implementation
git log -p -- frontend/viewer/package.json | grep -A10 -B10 "@stylistic/eslint-plugin"
```

Length of output: 0

---



</details>

**Review of ESLint Plugin Change in package.json**

The diff shows that the `@stylistic/eslint-plugin` version is now set to `"catalog:"`, but the git history check for this change returned no evidence of it being part of the i18n changes. Given the inconclusive git output, please verify manually whether this ESLint plugin modification was intentional and reviewed separately from the i18n implementation.

- File: `frontend/viewer/package.json` (line 38)
- Diff snippet:
  ```json
      "@stylistic/eslint-plugin": "catalog:",
  ```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T03:54:47Z
user: login: myieye
body: I spent about an hour trying to repro the unstable json with lingui examples and failed.

I tried a vite example as well as a none-vite example.
I guess it could be specific to the svelte lib.

There's [this line](https://github.com/lingui/js-lingui/blob/d36c323c2151f91b05305809bc9a6d291528aed2/packages/cli/src/api/catalog/mergeCatalog.ts#L42), which would move `translation` to the end of the object **if** `translation` isn't in `rest`. But...I'm not getting farther than that.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-07T14:32:02Z

Review comments:
diff_hunk: @@ -0,0 +1,21 @@
+﻿<script lang="ts">
+import {mdiTranslate} from '@mdi/js';
+import {MenuButton, type MenuOption} from 'svelte-ux';
+import {locale} from 'svelte-i18n-lingui';
+
+const languages: MenuOption[] = [
+  {value: 'en', label: 'English'},
+  {value: 'fr', label: 'Français'},
+  {value: 'es', label: 'Español'},
+];
+async function setLanguage(lang: string) {
+  let {messages} = await import(`../../locales/${lang}.json?lingui`);
+  locale.set(lang, messages);
+}
+</script>
+<MenuButton
+  options={languages}
+  value="en"
+  icon={mdiTranslate}
+  on:change={(e) => setLanguage(e.detail.value)}
+/>
path: frontend/viewer/src/lib/i18n/LocalizationPicker.svelte
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Initialize MenuButton with current locale**

The MenuButton is currently hardcoded to always show "en" as the selected value, regardless of the actual locale being used. This can create a confusing user experience when the displayed UI language doesn't match the selected language in the dropdown.


```diff
<MenuButton
  options={languages}
-  value="en"
+  value={$locale}
  icon={mdiTranslate}
  on:change={(e) => setLanguage(e.detail.value)}
/>
```

Additionally, you should consider adding a reactive statement to call `setLanguage` when the component is initialized, to ensure the translations are loaded:

```svelte
import {onMount} from 'svelte';

onMount(() => {
  setLanguage($locale);
});
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-05T06:10:54Z
author_association: NONE
diff_hunk: @@ -81,132 +84,141 @@
     }
   }
 
-
-  let projectsPromise = projectsService.localProjects().then(projects => projects.sort((p1, p2) => p1.name.localeCompare(p2.name)));
+  let projectsPromise = projectsService
+    .localProjects()
+    .then((projects) => projects.sort((p1, p2) => p1.name.localeCompare(p2.name)));
 
   async function refreshProjects() {
-    let promise = projectsService.localProjects().then(p => p.sort((p1, p2) => p1.name.localeCompare(p2.name)));
-    await promise;//avoids clearing out the list until the new list is fetched
+    let promise = projectsService.localProjects().then((p) => p.sort((p1, p2) => p1.name.localeCompare(p2.name)));
+    await promise; //avoids clearing out the list until the new list is fetched
     projectsPromise = promise;
   }
 
-
   const supportsTroubleshooting = useTroubleshootingService();
   let showTroubleshooting = false;
+
 </script>
-<AppBar title="Projects" class="bg-secondary min-h-12 shadow-md justify-between" menuIcon={null}>
+
+<AppBar title={$t`Dictionaries`} class="bg-secondary min-h-12 shadow-md justify-between" menuIcon={null}>
   <div slot="title" class="text-lg flex gap-2 items-center">
     <picture>
-      <source srcset={logoLight} media="(prefers-color-scheme: dark)">
-      <source srcset={logoDark} media="(prefers-color-scheme: light)">
-      <img src={logoDark} alt="Lexbox logo" class="h-6">
+      <source srcset={logoLight} media="(prefers-color-scheme: dark)" />
+      <source srcset={logoDark} media="(prefers-color-scheme: light)" />
+      <img src={logoDark} alt={$t`Lexbox logo`} class="h-6" />
     </picture>
-    <h3>Projects</h3>
+    <h3>{$t`Dictionaries`}</h3>
   </div>
   <div slot="actions" class="flex gap-2">
-    <Button
-      href={fwLiteConfig.feedbackUrl}
-      target="_blank"
-      size="sm"
-      variant="outline"
-      icon={mdiChatQuestion}>
-        Feedback
+    <Button href={fwLiteConfig.feedbackUrl} target="_blank" size="sm" variant="outline" icon={mdiChatQuestion}>
+      {$t`Feedback`}
     </Button>
     {#if supportsTroubleshooting}
-      <Button size="sm" variant="outline" icon={mdiFaceAgent} title="Troubleshoot" iconOnly={false}
-              on:click={() => showTroubleshooting = !showTroubleshooting}>
-      </Button>
-      <TroubleshootDialog bind:open={showTroubleshooting}/>
-    {/if}
-    <DevContent>
       <Button
-        href="/sandbox"
         size="sm"
         variant="outline"
-        icon={mdiTestTube}>
-        Sandbox
-      </Button>
+        icon={mdiFaceAgent}
+        title={$t`Troubleshoot`}
+        iconOnly={false}
+        on:click={() => (showTroubleshooting = !showTroubleshooting)}
+      ></Button>
+      <TroubleshootDialog bind:open={showTroubleshooting} />
+    {/if}
+    <DevContent>
+      <Button href="/sandbox" size="sm" variant="outline" icon={mdiTestTube}>Sandbox</Button>
     </DevContent>
+    <LocalizationPicker/>
   </div>
 </AppBar>
 <div class="mx-auto md:w-full md:py-4 max-w-2xl">
   <div class="flex-grow hidden md:block"></div>
   <div class="project-list">
     {#await projectsPromise}
-      <p>loading...</p>
+      <p>{$t`loading...`}</p>
     {:then projects}
       <div class="space-y-4 md:space-y-8">
         <div>
           <div class="flex flex-row">
-            <p class="sub-title">Local</p>
+            <p class="sub-title">{$t`Local`}</p>
             <div class="flex-grow"></div>
-            <Button icon={mdiRefresh}
-                    title="Refresh Projects"
-                    on:click={() => refreshProjects()}/>
+            <Button icon={mdiRefresh} title={$t`Refresh Projects`} on:click={() => refreshProjects()} />
           </div>
           <div>
-            {#each projects.filter(p => p.crdt) as project, i (project.id ?? i)}
+            {#each projects.filter((p) => p.crdt) as project, i (project.id ?? i)}
               {@const server = project.server}
               <AnchorListItem href={`/project/${project.name}`}>
-                <ListItem title={project.name}
-                          icon={mdiBookEditOutline}
-                          subheading={!server ? 'Local only' : ('Synced with ' + server.displayName)}
-                          loading={deletingProject === project.name}>
+                <ListItem
+                  title={project.name}
+                  icon={mdiBookEditOutline}
+                  subheading={!server ? $t`Local only` : $t`Synced with ${server.displayName}`}
path: frontend/viewer/src/home/HomeView.svelte
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider adding template variable type safety**

When using dynamic variables in translated strings like `$t\`Synced with ${server.displayName}\``, consider ensuring type safety for these template variables according to the i18n library's best practices.

---

🌐 Web query:

```web
How to ensure type safety for template variables in svelte-i18n-lingui?
```

💡 Result:

To ensure type safety for template variables in `svelte-i18n-lingui`, use **typed message descriptors** and leverage TypeScript's static analysis capabilities. Here's how to implement it:

### 1. Define Typed Message Descriptors
Use the `MessageDescriptor` interface to explicitly declare variable types:
```typescript
{$t({
  message: "Hello {name:string}! You have {count:number} messages.",
  values: { name: "Alice", count: 5 } // TypeScript validates these values
})}
```
TypeScript will automatically check:
- Required variables are provided
- Variable types match the descriptor
- No extra variables are included[8][2]

### 2. Custom Type Handling
For complex types, define them in a `custom-types.ts` file:
```typescript
// custom-types.ts
export type User = {
  id: string;
  name: string;
  email: string;
};

// Usage in component
{$t({
  message: "User details: {user:User}",
  values: { user: { id: "1", name: "Alice", email: "alice@example.com" } }
})}
```

### 3. Strict Extraction Pipeline
Configure your `lingui.config.ts` to enforce type safety during message extraction:
```typescript
// lingui.config.ts
import type { LinguiConfig } from "@lingui/conf";

const config: LinguiConfig = {
  locales: ["en", "es"],
  sourceLocale: "en",
  format: "po",
  catalogs: [{
    path: "src/locales/{locale}/messages",
    include: ["src/**/*.svelte"]
  }]
};
```
Run extraction to validate types:
```bash
npx lingui extract --strict
```

### 4. Component Slot Validation
For interpolated components, use typed slot parameters:
```svelte
<T msg="Click {link} for {details}">
  <a href="/">{linkText}</a>
  
  {#snippet details(slotParams: { type: string })}
    <strong>{slotParams.type} info</strong>
  {/snippet}
</T>
```
TypeScript validates both the slot content and parameters[8].

### Key Benefits
- **Compile-time error detection**: Catch missing/wrong variables before runtime[1][2]
- **Auto-completion**: IDE support for available variables and types[4][8]
- **Consistent validation**: Same type checks during development and CI/CD[8][10]
- **Documentation**: Message descriptors serve as type documentation[4][8]

For complex implementations, combine this with the library's `<T>` component for element interpolation and `$plural` store for type-safe pluralization handling[8][10].

Citations:

- 1: https://github.com/ivanhofer/typesafe-i18n
- 2: https://www.npmjs.com/package/typesafe-i18n/v/2.10.3
- 3: https://blog.aakashgoplani.in/internationalization-in-sveltekit-with-sveltekit-i18n
- 4: https://dev.to/ivanhofer/i18n-my-journey-to-a-simple-powerful-and-type-safe-solution-2c46
- 5: https://centus.com/blog/svelte-localization
- 6: https://crowdin.com/blog/2024/03/12/svelte-localization
- 7: https://phrase.com/blog/posts/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3/
- 8: https://github.com/HenryLie/svelte-i18n-lingui
- 9: https://phrase.com/blog/posts/how-to-localize-a-svelte-app-with-svelte-i18n/
- 10: https://lokalise.com/blog/svelte-i18n/

---



</details>

**Enhance Template Variable Type Safety in Translation Strings**

To improve reliability and leverage TypeScript’s static analysis, consider refactoring the dynamic translation call to use typed message descriptors. For example, update the code from:

```svelte
subheading={!server ? $t`Local only` : $t`Synced with ${server.displayName}`}
```

to something like:

```svelte
subheading={
  !server
    ? $t`Local only`
    : $t({
        message: "Synced with {displayName}",
        values: { displayName: server.displayName }
      })
}
```

This approach ensures that TypeScript validates the provided variable types and catches any mismatches at compile time.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-05T06:10:54Z
author_association: NONE
diff_hunk: @@ -10,27 +10,31 @@
     mdiRefresh,
     mdiTestTube,
   } from '@mdi/js';
-  import {AppBar, Button, ListItem, TextField} from 'svelte-ux';
+  import { AppBar, Button, ListItem, TextField } from 'svelte-ux';
   import flexLogo from '$lib/assets/flex-logo.png';
   import logoLight from '$lib/assets/logo-light.svg';
   import logoDark from '$lib/assets/logo-dark.svg';
-  import DevContent, {isDev} from '$lib/layout/DevContent.svelte';
+  import DevContent, { isDev } from '$lib/layout/DevContent.svelte';
   import {
     useFwLiteConfig,
     useImportFwdataService,
-    useProjectsService, useTroubleshootingService
+    useProjectsService,
+    useTroubleshootingService,
   } from '$lib/services/service-provider';
   import AnchorListItem from '$lib/utils/AnchorListItem.svelte';
   import TroubleshootDialog from '$lib/troubleshoot/TroubleshootDialog.svelte';
   import ServersList from './ServersList.svelte';
+  import { t } from 'svelte-i18n-lingui';
+  import LocalizationPicker from '$lib/i18n/LocalizationPicker.svelte';
path: frontend/viewer/src/home/HomeView.svelte
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Ensure locale files exist for all supported languages**

Make sure that all the locale files (en.json, fr.json, etc.) mentioned in the PR description have been created and include all the translatable strings from this component.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if locale files exist and contain the translations from this component
echo "Checking locale files..."
find frontend/viewer/locales -type f -name "*.json" | sort
echo ""

# Sample a few key translations to verify they exist in locale files
echo "Checking for 'Dictionaries' translation in locale files..."
rg -i "Dictionaries" frontend/viewer/locales
```

Length of output: 431

---



</details>

**Action Required: Locale Files Are Missing**

The expected locale files (e.g., en.json, fr.json) could not be found in the repository at `frontend/viewer/locales`. This likely indicates one of the following:
  
- The locale files have not been created yet.
- The locale files are located in a different directory than expected.
  
Please verify the intended location for these files and either create the missing files in the correct directory or update the localization configuration accordingly. Also, ensure that all necessary translation keys (such as "Dictionaries") are included in the locale files.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-05T06:10:54Z
author_association: NONE
diff_hunk: @@ -10,27 +10,31 @@
     mdiRefresh,
     mdiTestTube,
   } from '@mdi/js';
-  import {AppBar, Button, ListItem, TextField} from 'svelte-ux';
+  import { AppBar, Button, ListItem, TextField } from 'svelte-ux';
   import flexLogo from '$lib/assets/flex-logo.png';
   import logoLight from '$lib/assets/logo-light.svg';
   import logoDark from '$lib/assets/logo-dark.svg';
-  import DevContent, {isDev} from '$lib/layout/DevContent.svelte';
+  import DevContent, { isDev } from '$lib/layout/DevContent.svelte';
path: frontend/viewer/src/home/HomeView.svelte
user: login: myieye
body: Did you use VSCode for this? Your IDE used to remove these spaces 😉 
created_at: 2025-03-05T15:01:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+﻿import {jstsExtractor, svelteExtractor} from 'svelte-i18n-lingui/extractor';
+import {formatter} from "@lingui/format-json";
+import {defineConfig} from "@lingui/cli";
+
+export default defineConfig({
+  format: formatter({style: "lingui"}),
+  locales: ['en', 'es', 'fr'],
+  sourceLocale: 'en',
+  catalogs: [
+    {
+      path: 'src/locales/{locale}',
+      include: ['src'],
+      exclude: ['src/locales']
path: frontend/viewer/lingui.config.ts
user: login: myieye
body: This confuses me. Can you explain why we're excluding precisely the thing I would think we need to include?
created_at: 2025-03-05T15:03:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+﻿import {jstsExtractor, svelteExtractor} from 'svelte-i18n-lingui/extractor';
+import {formatter} from "@lingui/format-json";
+import {defineConfig} from "@lingui/cli";
+
+export default defineConfig({
+  format: formatter({style: "lingui"}),
+  locales: ['en', 'es', 'fr'],
+  sourceLocale: 'en',
+  catalogs: [
+    {
+      path: 'src/locales/{locale}',
+      include: ['src'],
+      exclude: ['src/locales']
path: frontend/viewer/lingui.config.ts
user: login: hahn-kev
body: the `path` specifies where the localization files live for the `include` sources. But we don't want to scan the localization files for text to localize
created_at: 2025-03-06T02:53:45Z
author_association: COLLABORATOR
diff_hunk: @@ -10,27 +10,31 @@
     mdiRefresh,
     mdiTestTube,
   } from '@mdi/js';
-  import {AppBar, Button, ListItem, TextField} from 'svelte-ux';
+  import { AppBar, Button, ListItem, TextField } from 'svelte-ux';
   import flexLogo from '$lib/assets/flex-logo.png';
   import logoLight from '$lib/assets/logo-light.svg';
   import logoDark from '$lib/assets/logo-dark.svg';
-  import DevContent, {isDev} from '$lib/layout/DevContent.svelte';
+  import DevContent, { isDev } from '$lib/layout/DevContent.svelte';
path: frontend/viewer/src/home/HomeView.svelte
user: login: hahn-kev
body: uh maybe? that is weird lol
created_at: 2025-03-06T02:54:31Z
author_association: COLLABORATOR
diff_hunk: @@ -117,7 +117,9 @@ export default [
     languageOptions: {
       parser: tsParser,
       parserOptions: {
-        project: true,
+        projectService: {
+          allowDefaultProject: ['lingui.config.ts'],
path: frontend/viewer/eslint.config.js
user: login: hahn-kev
body: @myieye what does this mean? `allowDefaultProject`?
created_at: 2025-03-06T02:55:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+﻿import {jstsExtractor, svelteExtractor} from 'svelte-i18n-lingui/extractor';
+import {formatter} from "@lingui/format-json";
+import {defineConfig} from "@lingui/cli";
+
+export default defineConfig({
+  format: formatter({style: "lingui"}),
+  locales: ['en', 'es', 'fr'],
+  sourceLocale: 'en',
+  catalogs: [
+    {
+      path: 'src/locales/{locale}',
+      include: ['src'],
+      exclude: ['src/locales']
path: frontend/viewer/lingui.config.ts
user: login: myieye
body: Gotcha
created_at: 2025-03-07T14:25:58Z
author_association: COLLABORATOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-05T06:06:19Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This pull request integrates internationalization throughout the Svelte frontend. The updates refactor documentation and UI components to remove static texts, replacing them with translatable entries via the Lingui library. A new Lingui configuration file, additional localization JSON files (English, Spanish, French), and related package updates have been introduced. A new language picker component allows dynamic locale switching. Overall, the changes focus on practical instructions and implementation of i18n support within the application.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `frontend/viewer/README.md` | Removed template introduction and technical guidance; content now centers on i18n usage examples and localization extraction commands. |
| `frontend/viewer/lingui.config.ts`, `frontend/viewer/vite.config.ts`, `frontend/viewer/package.json` | Added Lingui configuration and integrated the Lingui plugin into Vite; updated package scripts and dependencies to support i18n extraction and runtime internationalization. |
| `frontend/viewer/src/home/HomeView.svelte`, `frontend/viewer/src/home/Server.svelte`, `frontend/viewer/src/lib/auth/LoginButton.svelte` | Modified UI components to import the translation function and replace static strings with dynamic, translatable entries. |
| `frontend/viewer/src/lib/i18n/LocalizationPicker.svelte` | Introduced a new Svelte component for language selection that dynamically loads language JSON files and updates the locale. |
| `frontend/viewer/src/locales/en.json`, `frontend/viewer/src/locales/es.json`, `frontend/viewer/src/locales/fr.json` | Added new localization files containing key-value pairs for English, Spanish, and French translations to support the internationalization framework. |
| `frontend/viewer/eslint.config.js` | Modified `parserOptions` to enhance TypeScript project service configuration. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant U as User
    participant LP as LocalizationPicker
    participant JSON as Language File
    participant LI as Lingui Library
    participant C as UI Component

    U->>LP: Selects a language
    LP->>JSON: Dynamically import language JSON
    JSON-->>LP: Return localization strings
    LP->>LI: Update current locale
    LI-->>C: Provide translated text
    C->>U: Render UI with new language
```

## Poem

> In my warren of code I hop with glee,  
> Translations bloom as clear as can be.  
> With Lingui and JSON, the languages dance,  
> Each line a step in our i18n romance.  
> I twirl my ears in joyful cheer –  
> A rabbit’s code hops to a multilingual year!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=sillsdev/languageforge-lexbox&utm_content=1519):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-05T06:07:06Z
body: ## UI unit Tests
12 tests  ±0   12 :white_check_mark: ±0   0s :stopwatch: ±0s
 4 suites ±0    0 :zzz: ±0 
 1 files   ±0    0 :x: ±0 

Results for commit 25080d5a. ± Comparison against base commit ce471c6d.

[test-results]:data:application/gzip;base64,H4sIACIcyWcC/12MzQ6DIBAGX8Vw7mFZxGJfxiBCQqrSIJxM371b+oe97cx+mZ05P9uNXRp+atiWfSrQEkw56uTDSgiE9Ehlhx8YtmzMn7n6W70fnPbzQdgYQ3ybmNdv8Xkfgi/x6xWucoXrmgnL4hMBQwkKJqnHrgW0qATv8Iy9RC1AjK43HBQJYPcHZCZ0ff8AAAA=

:recycle: This comment has been updated with latest results.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-05T06:08:31Z
body: ## C# Unit Tests
107 tests  ±0   107 :white_check_mark: ±0   5s :stopwatch: ±0s
 16 suites ±0     0 :zzz: ±0 
  1 files   ±0     0 :x: ±0 

Results for commit 25080d5a. ± Comparison against base commit ce471c6d.

[test-results]:data:application/gzip;base64,H4sIAIccyWcC/13MSw6CMBSF4a2Qjh3cFluKmzGltMmNQE0fI+PevRYVcPj9JzkP5nFyiV0afmpYKphXKNJYoskYFrIk0pLrBt1X11Ss/U83vFOCX/AGp0NwMYb4KbEs2+cbx8u1bI/Vu8Pq/Z8N84yZwIQEDaM0gzqDcEK3XIlO9FKYFtrB95aDpgDs+QLWXD0SBAEAAA==

:recycle: This comment has been updated with latest results.
author_association: CONTRIBUTOR
user: login: hahn-kev
created_at: 2025-03-06T03:55:44Z
body: Alright so I've fixed some stuff from your feedback and CodeRabbit. I also changed the extract to run twice and that solves the json settling issue. Maybe we want to make an issue in lingui for it?

As for the CI check. We can actually run `lingui compile --strict` and that will fail if there's missing messages. However that's already what the vite plugin is doing, with strict set to false, so I'm thinking we just open a PR to make that configurable in the plugin, then we can make it strict in prod builds and not strict in dev builds.
author_association: COLLABORATOR