Skip to content

feat(i18n): migrate Electron translations to i18next with symbolic keys#134

Merged
kilbot merged 10 commits intomainfrom
feature/i18next-migration-v2
Feb 6, 2026
Merged

feat(i18n): migrate Electron translations to i18next with symbolic keys#134
kilbot merged 10 commits intomainfrom
feature/i18next-migration-v2

Conversation

@kilbot
Copy link
Copy Markdown
Contributor

@kilbot kilbot commented Feb 4, 2026

Summary

  • Migrate from Transifex Native to i18next with a custom ElectronStoreBackend
  • Replace English string keys with structured symbolic keys (menu.check_for_updates instead of Check for Updates)
  • Bundle en/electron.json as fallback — users never see raw symbolic keys
  • Add regional locale fallback in backend (e.g. fr_CAfr)
  • Graceful error handling (return empty data on fetch failure, let bundled English handle it)
  • Update CDN URL to CalVer format (2026.2.0)

Key format

feature.descriptor with flat keys (keySeparator: false):

  • menu.* — app menu items
  • update.* — auto-updater dialogs
  • app.* — app-level operations (hardware accel, clear data)
  • common.* — shared (Yes/No)

i18next config

  • fallbackLng: 'en' with bundled resources: { en: { electron: en } }
  • load: 'currentOnly' — backend handles regional→base fallback internally
  • Custom interpolation: {variable} (not {{variable}})

Test plan

  • Verify Electron app loads translations for non-English system locales
  • Verify cached translations are reused on subsequent launches
  • Verify English fallback displays correctly when no translations exist for a locale
  • Verify regional locale fallback (e.g. fr_CA falls back to fr if fr_CA not available)
  • Verify all menu items, dialogs, and update prompts display English text (not symbolic keys)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores

    • Switched translation system to i18next and updated dependency.
    • Standardized menu labels, dialogs, update prompts and progress messages to use new translation keys.
    • Replaced legacy translation cache with a CDN-backed cache and added automation to manage translation version updates.
  • New Features

    • Added an English locale with refined UI strings for menus, confirmations and update flows.

Replace @transifex/native with i18next using a custom ElectronStoreBackend
that fetches translations from jsDelivr CDN and caches them in electron-store.
Remove _tags from all t() calls since namespace is now set via defaultNS.
The ElectronStoreBackend.read() was calling i18next.addResourceBundle()
on the global singleton, but the app creates a separate instance via
createInstance(). Background CDN fetches were updating the wrong store.
The translations repo now nests JS files under a monorepo/ subdirectory.
Also bump TRANSLATION_VERSION from 1.7.8 to 1.8.7.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 4, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Replaces Transifex Native with i18next and a new ElectronStore-backed backend, removes CustomCache, updates package.json dependency, replaces many translation keys (removing { _tags: 'electron' }) across main and menu files, adds an English locale JSON, and adds a workflow to bump translation version.

Changes

Cohort / File(s) Summary
Dependency
package.json
Replaced @transifex/native with i18next in dependencies.
i18next backend & init
src/main/translations/index.ts
Removed Transifex init; added i18next instance with a custom ElectronStoreBackend that reads/writes electron-store, fetches translations from jsDelivr, updates i18next resource bundles, and exports t bound to the instance plus loadTranslations.
Cache removal
src/main/translations/cache.ts
Deleted the CustomCache implementation and related imports/exports.
Locale additions
src/main/translations/locales/en/electron.json
Added English locale file containing app.*, menu.*, update.*, common.* keys.
Translation key updates — core
src/main/clear-data.ts, src/main/config.ts, src/main/update.ts, src/main/progress-bar.ts
Replaced translation calls that used { _tags: 'electron' } with new keys (e.g., app.*, update.*, common.yes/common.no); no control-flow changes.
Translation key updates — menus
src/main/menu/...
src/main/menu/app.ts, src/main/menu/file.ts, src/main/menu/help.ts, src/main/menu/view.ts, src/main/menu/window.ts
Replaced menu label translation calls with keys like menu.file, menu.check_for_updates, menu.advanced, menu.performance, etc.; handlers and submenu structures unchanged.
CI workflow
.github/workflows/update-translations.yml
Added workflow to update TRANSLATION_VERSION in src/main/translations/index.ts from a repository_dispatch payload and commit/push changes when needed.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant I18n as i18nInstance
    participant Backend as ElectronStoreBackend
    participant Store as Electron Store
    participant CDN as jsDelivr CDN

    App->>I18n: loadTranslations(lang)
    I18n->>Backend: read(language, namespace, callback)
    Backend->>Store: check cache for language
    alt Cache hit
        Store-->>Backend: cached translations
        Backend-->>I18n: callback(cached data)
        I18n-->>App: translations available
        Note over Backend,CDN: optionally refresh in background
        Backend->>CDN: fetch fresh translations
        CDN-->>Backend: fresh data
        Backend->>Store: update cache
        Backend->>I18n: addResourceBundle(fresh data)
    else Cache miss
        Backend->>CDN: fetch translations
        CDN-->>Backend: fetched data
        Backend->>Store: save cache
        Backend-->>I18n: callback(fetched data)
        I18n-->>App: translations available
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I swapped tx for i18next bright,
Caches curled in store by night.
CDN brings phrases new,
Keys renamed, tags withdrew—
Hop, translate, and nibble delight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: migrating from Transifex Native to i18next with symbolic translation keys, which is the primary focus across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/i18next-migration-v2

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/main/config.ts`:
- Around line 53-60: The Prettier failure is caused by the multiline ternary
inside the translation call for message (the t('Hardware acceleration ...', {
enabled: enabled ? t('enabled') : t('disabled') }) expression). Fix it by
formatting the ternary as a single line so Prettier accepts it: locate the
translation call that sets message (the t(..., { enabled: ... }) block) and
collapse the enabled ternary into one line (enabled: enabled ? t('enabled') :
t('disabled')) while keeping surrounding indentation and commas intact.

In `@src/main/translations/index.ts`:
- Around line 36-64: The read method currently calls callback(null) immediately
on a cache miss causing first-run locales to render fallbacks; change
read(language, namespace, callback) so that when this.store.get(language) is
undefined you perform the fetch and only invoke callback(null, data) after the
fetch and JSON parse succeed (or callback(err) on fatal fetch/parsing error),
while preserving the existing behavior for cached entries (keep returning cached
via callback(null, cached) and still do the background fetch/refresh logic);
update references to this.store, the fetch URL using TRANSLATION_VERSION, and
this.services.resourceStore.addResourceBundle to be used inside the fetch
resolution path for the cache-miss case.
🧹 Nitpick comments (2)
src/main/update.ts (1)

120-122: Remove the empty options object in the update-installed message.

The second argument is now empty after tag removal; dropping it keeps the callsite cleaner and consistent with other t() calls in the codebase.

♻️ Suggested cleanup
-						message: t('Updates downloaded, application will restart for update to take effect.', {
-						}),
+						message: t('Updates downloaded, application will restart for update to take effect.'),
src/main/translations/index.ts (1)

87-113: Nice: instance-scoped t keeps lookups tied to the configured backend.

Consider using getFixedT(null, 'electron') as an alternative to binding if you want to add namespace scoping or namespace-specific t functions.

- Collapse ternary to single line in config.ts (Prettier fix)
- Defer callback on cache miss until CDN fetch resolves so first-run
  non-English locales get translations immediately
- Update TRANSLATION_VERSION to 2026.2.0 (CalVer)
- Remove v prefix from CDN URL to match new release tagging
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/main/translations/index.ts`:
- Around line 64-67: The catch block currently calls callback(err, false) which
uses a non-standard i18next backend read signature; update the error path in the
promise rejection to invoke the read callback as callback(err) (or callback(err,
undefined/null)) instead of passing false so i18next treats it as an error with
no resource data — modify the .catch handler where log.error is called and the
callback is invoked (referencing log.error and the callback used in this module)
to pass only the error.
🧹 Nitpick comments (2)
src/main/translations/index.ts (2)

46-48: Silent failure path when response is not OK leaves fetch chain in ambiguous state.

When response.ok is false, return; passes undefined to the next .then(), which then falls through to the else if (!cached) branch calling callback(null, {}). This silently treats HTTP errors (404, 500, etc.) as "no translations available" rather than actual errors. Consider explicitly handling non-OK responses.

♻️ Proposed fix to handle HTTP errors explicitly
 		fetch(url)
 			.then((response) => {
-				if (!response.ok) return;
+				if (!response.ok) {
+					throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+				}
 				return response.json();
 			})

71-73: Stale comment references removed Transifex system.

The comment still mentions "Transifex-compatible locale codes" but this PR migrates away from Transifex to i18next.

📝 Proposed fix
 /**
- * Map system locale codes to Transifex-compatible locale codes.
+ * Map system locale codes to supported translation locale codes.
  */

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/main/translations/index.ts`:
- Around line 110-114: Guard against an empty preferred-locale list by checking
the array returned from app.getPreferredSystemLanguages(); if systemLocales[0]
is undefined, fall back to 'en' before passing into getLocaleFromCode. Update
the code that assigns systemLocales and systemLocale (references:
app.getPreferredSystemLanguages(), getLocaleFromCode, variable systemLocale) so
you compute const systemLocale = getLocaleFromCode(systemLocales[0] ?? 'en') (or
equivalent null/empty check) and then call
i18nInstance.changeLanguage(systemLocale).
- Line 18: Update the TRANSLATION_VERSION constant (symbol: TRANSLATION_VERSION
in src/main/translations/index.ts) from '2026.2.0' to the correct release tag
'v1.8.7'; after changing the value, verify the CDN (jsDelivr) serves the file
and if it still returns 404 coordinate a redeploy of the wcpos/translations
release or request a jsDelivr cache refresh to ensure translations load
correctly.
🧹 Nitpick comments (1)
src/main/translations/index.ts (1)

36-56: Cache key should include namespace to avoid collisions.

read receives namespace but the store key uses only language, which will clash if you add more namespaces later. Consider keying by both values.

♻️ Suggested change
 read(language: string, namespace: string, callback: (err: any, data?: any) => void) {
-    const cached = this.store.get(language) as TranslationRecord | undefined;
+    const cacheKey = `${language}:${namespace}`;
+    const cached = this.store.get(cacheKey) as TranslationRecord | undefined;
     if (cached) {
         log.debug(`Loading ${language} translations from cache`);
         callback(null, cached);
     }

     // Fetch fresh translations from jsDelivr in the background
     const url = `https://cdn.jsdelivr.net/gh/wcpos/translations@${TRANSLATION_VERSION}/translations/js/${language}/monorepo/${namespace}.json`;
     fetch(url)
       .then((response) => {
         if (!response.ok) return;
         return response.json();
       })
       .then((data) => {
         if (data && Object.keys(data).length > 0) {
-          const current = this.store.get(language) as TranslationRecord | undefined;
+          const current = this.store.get(cacheKey) as TranslationRecord | undefined;
           if (JSON.stringify(current) !== JSON.stringify(data)) {
-            this.store.set(language, data);
+            this.store.set(cacheKey, data);
           }
           this.services.resourceStore.addResourceBundle(language, namespace, data, true, true);
           if (!cached) {
             callback(null, data);
           }


const store = new Store<Record<string, TranslationRecord>>();
const cache = new CustomCache(store);
const TRANSLATION_VERSION = '2026.2.0';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify CDN availability for candidate translation tags
for v in 2026.2.0 1.8.7 v1.8.7; do
  url="https://cdn.jsdelivr.net/gh/wcpos/translations@${v}/translations/js/en/monorepo/electron.json"
  printf "%s -> " "$v"
  curl -s -o /dev/null -w "%{http_code}\n" "$url"
done

Repository: wcpos/electron

Length of output: 100


🏁 Script executed:

# Check for PR context, version references, or objectives in the repository
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "*.json" \) -not -path "./node_modules/*" -not -path "./.git/*" | head -20 | xargs grep -l "1.8.7\|2026.2.0\|TRANSLATION_VERSION" 2>/dev/null | head -10

Repository: wcpos/electron

Length of output: 40


🏁 Script executed:

# Also check the index.ts file directly for context
cat -n src/main/translations/index.ts | head -30

Repository: wcpos/electron

Length of output: 996


🏁 Script executed:

# Check package.json and lock files for translation version references
find . -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" | xargs grep -i "translat" 2>/dev/null | head -20

Repository: wcpos/electron

Length of output: 40


🏁 Script executed:

# Check git tags or recent commits that might reference translations version
git log --oneline --all -20 | grep -i "translat\|version"

Repository: wcpos/electron

Length of output: 124


🏁 Script executed:

# Check if there's any documentation or comments in the codebase about which version to use
rg "translations.*@" --type ts --type js -B 2 -A 2 2>/dev/null | head -40

Repository: wcpos/electron

Length of output: 455


🏁 Script executed:

# Check what other versions might be available by looking at jsDelivr structure
# Try to list available tags from wcpos/translations repository via GitHub API
curl -s "https://api.github.com/repos/wcpos/translations/releases?per_page=10" 2>/dev/null | jq -r '.[].tag_name' 2>/dev/null || echo "GitHub API check failed"

Repository: wcpos/electron

Length of output: 64


Fix TRANSLATION_VERSION to match the correct release tag.

The constant is set to 2026.2.0, which does not exist as a release. The correct tag is v1.8.7 (confirmed in wcpos/translations releases). However, the CDN currently returns 404 for this tag, which will break translation loading. Update the constant to the correct version:

-const TRANSLATION_VERSION = '2026.2.0';
+const TRANSLATION_VERSION = 'v1.8.7';

If the CDN still returns 404 after this change, the release may need to be redeployed or the jsDelivr cache needs to be refreshed.

📝 Committable suggestion

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

Suggested change
const TRANSLATION_VERSION = '2026.2.0';
const TRANSLATION_VERSION = 'v1.8.7';
🤖 Prompt for AI Agents
In `@src/main/translations/index.ts` at line 18, Update the TRANSLATION_VERSION
constant (symbol: TRANSLATION_VERSION in src/main/translations/index.ts) from
'2026.2.0' to the correct release tag 'v1.8.7'; after changing the value, verify
the CDN (jsDelivr) serves the file and if it still returns 404 coordinate a
redeploy of the wcpos/translations release or request a jsDelivr cache refresh
to ensure translations load correctly.

… fallback

Replace English string keys with structured feature.descriptor keys
(e.g. t('menu.check_for_updates') instead of t('Check for Updates')).

- Rewrite all t() calls to use symbolic keys
- Bundle en/electron.json as fallback (fallbackLng: 'en')
- Add regional locale fallback in backend (fr_CA → fr)
- Graceful error handling (return empty data, let fallback handle it)
@kilbot kilbot changed the title feat(i18n): migrate Electron translations to i18next feat(i18n): migrate Electron translations to i18next with symbolic keys Feb 6, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@src/main/config.ts`:
- Line 57: The translation keys used in the config block—t('enabled') and
t('disabled')—are missing from the locale resource; update the calls to use
namespaced keys (for example t('app.enabled') and t('app.disabled')) and add
corresponding entries to the locale JSON (electron.json) or alternatively add
the plain 'enabled' and 'disabled' keys to the locale file; ensure you update
every occurrence of t('enabled')/t('disabled') so the i18n lookup (function t)
resolves correctly.
- Around line 54-59: Prettier is failing on the multiline t(...) call used to
set message in config (the t function call with the ternary using enabled).
Reformat the call so it conforms to Prettier: put the t(...) call and its object
argument on a single logical line or align properties per Prettier rules, and
simplify the ternary expression (enabled ? t('enabled') : t('disabled')) to a
single expression without extra line breaks; update the message assignment where
t(...) is used to ensure consistent spacing and commas so Prettier CI passes.

In `@src/main/translations/index.ts`:
- Around line 46-51: The fetchTranslations method's parameter list needs to be
wrapped to satisfy the formatter: split the parameters of
fetchTranslations(language: string, namespace: string) across multiple lines
(one parameter per line) and adjust the method signature accordingly in the
fetchTranslations function so formatting checks pass; keep the same parameter
names and types and ensure the body still calls this.buildUrl(language,
namespace) and returns the same Promise<TranslationRecord | null>.
- Around line 54-86: The read method returns cached translations without
considering translation version, so stale translations persist after a
TRANSLATION_VERSION bump; update read (and any caching logic around this.store)
to include the translation version in the cache key or explicitly
invalidate/refresh cached entries when TRANSLATION_VERSION changes: e.g., derive
keys from `${language}:${TRANSLATION_VERSION}` when calling this.store.get/set
or check a stored version on load and call this.store.delete/refresh before
returning cached data; update functions fetchTranslations and any logic that
sets the cache so they use the same versioned key or invalidation flow
(referencing read, fetchTranslations, getBaseLanguage, and this.store).

In `@src/main/translations/locales/en/electron.json`:
- Around line 1-28: Add the missing translation keys for the
hardware-acceleration interpolation by defining "app.enabled" and "app.disabled"
in the electron.json locale (e.g., "app.enabled": "Enabled", "app.disabled":
"Disabled"), then update the code that calls t('enabled')/t('disabled') in
config.ts to use the namespaced keys (e.g., replace t('enabled') with
t('app.enabled') and t('disabled') with t('app.disabled')) so the interpolated
{enabled} value renders the localized string.

In `@src/main/update.ts`:
- Around line 120-122: The message call is passing an empty interpolation object
to the translator; remove the unused empty object so the call becomes
t('update.updates_downloaded_application_will_restart_for') instead of
t('update.updates_downloaded_application_will_restart_for', {}), i.e. update the
object property named message (where t(...) is invoked) to pass only the
translation key.

Comment on lines +1 to +28
{
"app.are_you_sure": "Are you sure?",
"app.cleared_app_data": "Cleared app data",
"app.could_not_clear_app_data": "Could not clear app data",
"app.hardware_acceleration": "Hardware Acceleration",
"app.hardware_acceleration_application_will_restart_for": "Hardware acceleration {enabled}, application will restart for update to take effect.",
"common.no": "No",
"common.yes": "Yes",
"menu.advanced": "Advanced",
"menu.check_for_updates": "Check for Updates",
"menu.clear_app_data": "Clear App Data",
"menu.documentation": "Documentation",
"menu.enable_hardware_acceleration": "Enable Hardware Acceleration",
"menu.f_a_q": "F.A.Q.",
"menu.file": "File",
"menu.performance": "Performance",
"menu.report_an_issue": "Report an Issue",
"menu.request_a_feature": "Request a Feature",
"menu.view": "View",
"update.a_new_version_is_available_do": "A new version ({version}) is available. Do you want to update now?",
"update.current_version_is_up-to-date": "Current version is up-to-date.",
"update.downloading_update": "Downloading update...",
"update.found_updates": "Found Updates",
"update.install_updates": "Install Updates",
"update.no_updates": "No Updates",
"update.remind_me_later": "Remind me later",
"update.updates_downloaded_application_will_restart_for": "Updates downloaded, application will restart for update to take effect."
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing translation keys for enabled and disabled.

The config.ts file (line 57) calls t('enabled') and t('disabled') for the hardware acceleration interpolation, but these keys are not defined in this locale file. This will cause the raw keys to be displayed instead of translated text.

🔧 Proposed fix to add missing keys
 {
 	"app.are_you_sure": "Are you sure?",
 	"app.cleared_app_data": "Cleared app data",
 	"app.could_not_clear_app_data": "Could not clear app data",
+	"app.disabled": "disabled",
+	"app.enabled": "enabled",
 	"app.hardware_acceleration": "Hardware Acceleration",

Then update config.ts to use namespaced keys:

enabled: enabled ? t('app.enabled') : t('app.disabled'),
🤖 Prompt for AI Agents
In `@src/main/translations/locales/en/electron.json` around lines 1 - 28, Add the
missing translation keys for the hardware-acceleration interpolation by defining
"app.enabled" and "app.disabled" in the electron.json locale (e.g.,
"app.enabled": "Enabled", "app.disabled": "Disabled"), then update the code that
calls t('enabled')/t('disabled') in config.ts to use the namespaced keys (e.g.,
replace t('enabled') with t('app.enabled') and t('disabled') with
t('app.disabled')) so the interpolated {enabled} value renders the localized
string.

- Fix Prettier formatting in config.ts and translations/index.ts
- Add missing app.enabled/app.disabled translation keys
- Use namespaced t('app.enabled')/t('app.disabled') in config.ts
- Remove empty interpolation object in update.ts
- Version cache key to invalidate stale translations on version bump
- Guard against empty getPreferredSystemLanguages() result
- Fix stale "Transifex-compatible" comment
Listens for translation-release dispatch from wcpos/translations repo
and bumps TRANSLATION_VERSION in src/main/translations/index.ts.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.github/workflows/update-translations.yml:
- Around line 13-18: Validate and sanitize the incoming VERSION before using it
in the sed substitution: add a check in the workflow that ensures the VERSION
env (from github.event.client_payload.version) matches an allowed pattern (e.g.,
semantic version or a strict /^[0-9A-Za-z._-]+$/ regex) and fail early if it
doesn't; then either escape shell-special characters (quotes, slashes,
ampersands) in VERSION or use a safer substitution mechanism to update
TRANSLATION_VERSION in src/main/translations/index.ts instead of interpolating
raw $VERSION into the sed command.
🧹 Nitpick comments (1)
.github/workflows/update-translations.yml (1)

7-11: Consider adding explicit permissions for least-privilege security.

The workflow pushes commits but lacks an explicit permissions block. Declaring minimum required permissions is a GitHub Actions security best practice.

🔒 Proposed fix to add explicit permissions
 jobs:
   update:
     runs-on: ubuntu-latest
+    permissions:
+      contents: write
     steps:
       - uses: actions/checkout@v4

Reject non-CalVer input to prevent sed injection or malformed
substitutions from repository_dispatch payloads.
@kilbot kilbot merged commit 5e40702 into main Feb 6, 2026
3 of 4 checks passed
@kilbot kilbot deleted the feature/i18next-migration-v2 branch February 6, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant