Skip to content

Commit 5f6fc5a

Browse files
committed
v0.6.6
1 parent 5c4587e commit 5f6fc5a

File tree

8 files changed

+72
-55
lines changed

8 files changed

+72
-55
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
Open the extension's Options page to select your language. In the Webflow Designer, you can also switch languages instantly using the dropdown menu in the settings.
2929

30-
Click the toolbar icon to toggle translations at any time. This acts as a master switch — the badge will show OFF when translations are disabled.
30+
Click the toolbar icon to toggle translations at any time. This acts as a master switch.
3131

3232
It now supports Japanese, Traditional Chinese, Simplified Chinese, Korean, Thai, French, and Italian, with more languages to be added over time.
3333

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/chrome-manifest.json",
33
"manifest_version": 3,
4-
"version": "0.6.5",
4+
"version": "0.6.6",
55
"default_locale": "en",
66
"name": "__MSG_appName__",
77
"description": "__MSG_appDescription__",

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"private": true,
33
"name": "webflow-ui-localization",
44
"description": "Translate the Webflow Dashboard and Designer UI into Japanese, Chinese, Korean, and more!",
5-
"version": "0.6.5",
5+
"version": "0.6.6",
66
"license": "MIT",
77
"type": "module",
88
"author": {

src/_new_lang_checklist.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
To add a new language, update these files:
44

5-
- `src/types.ts`: Add 'fr' to LanguageCode type.
5+
- `src/types.ts`: Add '*' to LanguageCode type (* is the language code).
66
- `src/locales/*.json`: Prepare the MAIN translations (via POEditor).
77
- `src/locales-extension/*.json`: Prepare the extension's UI translations.
88
- `src/content/scripts.ts`: Import `src/locales/*.json` and add to BUNDLED_LANGUAGES object.
99
- `src/options/OptionsApp.ts`: Import `src/locales-extension/*.json`, add to LANGUAGES array, and add to EXTENSION_LOCALES.
1010
- `src/content/injections.ts`: Add new language options.
1111
- `.github/scripts/pull-poeditor.mjs`: Add 'fr' to the list for pulling from POEditor.
1212
- `_locales/*/messages.json`: Create extension metadata for Chrome Web Store.
13-
- `verify_json.mjs`: Add to the list of json files to verify.
1413

1514
Content updates:
1615

src/options/OptionsApp.ts

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,21 @@ const FALLBACK_STRINGS: Dictionary = {
9999

100100
// Safely retrieve the storage area (Sync if available, otherwise Local)
101101
function getStorage(): chrome.storage.SyncStorageArea | chrome.storage.LocalStorageArea {
102-
return chrome?.storage?.sync || chrome?.storage?.local
102+
const sync = chrome?.storage?.sync;
103+
const local = chrome?.storage?.local;
104+
if (sync) return sync;
105+
if (local) return local;
106+
throw new Error('Neither chrome.storage.sync nor chrome.storage.local is available in this environment.');
103107
}
104108

105109
// Heavier cache reads (like the large translation map) should come from 'local' storage
106110
// when available to avoid hitting 'sync' storage quotas and latency.
107111
function getCacheStorage(): chrome.storage.LocalStorageArea | chrome.storage.SyncStorageArea {
108-
return chrome?.storage?.local || chrome?.storage?.sync
112+
const local = chrome?.storage?.local;
113+
const sync = chrome?.storage?.sync;
114+
if (local) return local;
115+
if (sync) return sync;
116+
throw new Error('Neither chrome.storage.local nor chrome.storage.sync is available in this environment.');
109117
}
110118

111119
// Get a localized string for the Options UI (fallback to English)
@@ -139,30 +147,31 @@ function normalizeSettings(raw: Partial<Settings>): Settings {
139147
// STATE MANAGEMENT
140148
// ---------------------------------------------------------------------------
141149

142-
// Tracks the last rendered language to decide when a full re-render is needed
143-
let lastRenderedLanguage: LanguageCode | null = null
144-
// Holds the current application state
145-
let currentSettings: Settings = { ...DEFAULT_SETTINGS }
146-
// Latest locale source metadata (per language).
147-
// This tells us if we are using the bundled JSON or a fresher version from CDN.
148-
let latestLocaleMeta: Record<Exclude<LanguageCode, 'off'>, LocaleMeta> | null = null
149-
// Track if a manual refresh is in progress to prevent UI flickering
150-
let isManuallyRefreshing = false
150+
// Centralized state object for the options page.
151+
const appState = {
152+
lastRenderedLanguage: null as LanguageCode | null,
153+
currentSettings: { ...DEFAULT_SETTINGS } as Settings,
154+
// Latest locale source metadata (per language).
155+
// This tells us if we are using the bundled JSON or a fresher version from CDN.
156+
latestLocaleMeta: null as Record<Exclude<LanguageCode, 'off'>, LocaleMeta> | null,
157+
// Track if a manual refresh is in progress to prevent UI flickering
158+
isManuallyRefreshing: false
159+
}
151160

152161
// ---------------------------------------------------------------------------
153162
// DOM RENDERING
154163
// ---------------------------------------------------------------------------
155164

156165
// Main render: full re-render on language change, otherwise just sync values
157166
function renderApp(settings: Settings) {
158-
currentSettings = settings
167+
appState.currentSettings = settings
159168
const root = document.getElementById('root')
160169
if (!root) return
161170

162171
// Full re-render needed if language changed (to update UI text).
163172
// We must re-bind events because the DOM nodes are replaced.
164-
if (lastRenderedLanguage !== settings.language) {
165-
lastRenderedLanguage = settings.language
173+
if (appState.lastRenderedLanguage !== settings.language) {
174+
appState.lastRenderedLanguage = settings.language
166175
renderFullPage(root, settings)
167176
bindEvents(root)
168177
}
@@ -173,8 +182,8 @@ function renderApp(settings: Settings) {
173182
// Only update badge if not in the middle of a manual refresh result.
174183
// This prevents the "Done" message from being immediately overwritten by "Bundled"
175184
// before the user has a chance to see it.
176-
if (!isManuallyRefreshing) {
177-
updateLocaleBadge(root, latestLocaleMeta, settings)
185+
if (!appState.isManuallyRefreshing) {
186+
updateLocaleBadge(root, appState.latestLocaleMeta, settings)
178187
}
179188
}
180189

@@ -422,12 +431,12 @@ function bindEvents(root: HTMLElement) {
422431
// Optimistic UI update: Toggle the switch visually immediately.
423432
// The actual storage save happens asynchronously below.
424433
if (key === 'enabled') {
425-
updateValues(root, { ...currentSettings, enabled: val })
434+
updateValues(root, { ...appState.currentSettings, enabled: val })
426435
}
427436

428437
// Save to storage
429438
storage.set({ [key]: val }, () => {
430-
setStatusMsg(root, getText(currentSettings.language, 'options_saved_msg'))
439+
setStatusMsg(root, getText(appState.currentSettings.language, 'options_saved_msg'))
431440
})
432441
})
433442
})
@@ -441,32 +450,32 @@ function bindEvents(root: HTMLElement) {
441450

442451
// Saving language triggers 'onChanged', which will call renderApp() and re-render the page.
443452
storage.set({ language: val, enabled: true }, () => {
444-
setStatusMsg(root, getText(currentSettings.language, 'options_saved_msg'))
453+
setStatusMsg(root, getText(appState.currentSettings.language, 'options_saved_msg'))
445454
})
446455

447456
// Updates internal state loosely until the re-render happens
448-
currentSettings.language = val
449-
currentSettings.enabled = true
457+
appState.currentSettings.language = val
458+
appState.currentSettings.enabled = true
450459
}
451460
})
452461

453462
// Badge click handler (Force Refresh)
454463
const badge = root.querySelector('#cdn_json_badge')
455464
badge?.addEventListener('click', () => {
456465
// Only allow refresh if using CDN (checked via valid class or settings)
457-
if (!currentSettings.useCdn) return
466+
if (!appState.currentSettings.useCdn) return
458467

459468
const el = badge as HTMLElement
460469
// 1. Set Loading Text
461-
el.textContent = getText(currentSettings.language, 'options_refreshing_msg')
462-
isManuallyRefreshing = true
470+
el.textContent = getText(appState.currentSettings.language, 'options_refreshing_msg')
471+
appState.isManuallyRefreshing = true
463472

464473
chrome.storage.local.remove(LOCALE_CACHE_KEY, () => {
465474
// 2. Set Done Text on completion
466475
// We keep isManuallyRefreshing = true so onChanged doesn't overwrite this with "Bundled"
467476
// It stays true so this message persists until the user reloads the Page
468477
// or until a NEW cache entry appears (which happens when they visit Webflow).
469-
el.textContent = getText(currentSettings.language, 'options_refresh_done_msg')
478+
el.textContent = getText(appState.currentSettings.language, 'options_refresh_done_msg')
470479
})
471480
})
472481

@@ -501,7 +510,7 @@ function bindEvents(root: HTMLElement) {
501510
setTimeout(() => status.classList.remove('visible'), 2000)
502511
}
503512
// Update local state
504-
currentSettings.exclusionSelectors = lines
513+
appState.currentSettings.exclusionSelectors = lines
505514
})
506515
})
507516
}
@@ -516,8 +525,16 @@ function bindEvents(root: HTMLElement) {
516525
let resetConfirming = false
517526
let resetConfirmTimeout: ReturnType<typeof setTimeout> | undefined
518527

519-
const defaultResetLabel = getText(currentSettings.language, 'options_advanced_reset')
520-
const confirmResetLabel = getText(currentSettings.language, 'options_advanced_reset_confirm')
528+
// Clean up the timeout if the page is unloaded, to prevent memory leaks
529+
window.addEventListener('beforeunload', () => {
530+
if (resetConfirmTimeout) {
531+
clearTimeout(resetConfirmTimeout)
532+
resetConfirmTimeout = undefined
533+
}
534+
});
535+
536+
const defaultResetLabel = getText(appState.currentSettings.language, 'options_advanced_reset')
537+
const confirmResetLabel = getText(appState.currentSettings.language, 'options_advanced_reset_confirm')
521538

522539
const setResetState = (confirming: boolean) => {
523540
resetConfirming = confirming
@@ -542,7 +559,7 @@ function bindEvents(root: HTMLElement) {
542559
const status = root.querySelector('.save_status') as HTMLElement
543560

544561
const defaults = getDefaultExclusionSelectors()
545-
const updatedSettings = { ...currentSettings, exclusionSelectors: defaults }
562+
const updatedSettings = { ...appState.currentSettings, exclusionSelectors: defaults }
546563

547564
// Update UI immediately
548565
if (textarea) textarea.value = defaults.join('\n')
@@ -572,7 +589,7 @@ function setStatusMsg(root: HTMLElement, msg: string) {
572589
setTimeout(() => {
573590
// Revert to idle message if still in 'changed' state
574591
if (el.dataset.status === 'changed') {
575-
el.textContent = getText(currentSettings.language, 'options_status_idle')
592+
el.textContent = getText(appState.currentSettings.language, 'options_status_idle')
576593
el.dataset.status = 'idle'
577594
}
578595
}, 2000)
@@ -589,9 +606,9 @@ export default function initOptionsPage() {
589606

590607
// 1. Initial Load: Get settings and cache meta from storage.
591608
// We fetch both user settings (sync/local) and the translation cache metadata (local).
592-
storage.get({ ...DEFAULT_SETTINGS, exclusionSelectors: getDefaultExclusionSelectors() }, (items) => {
609+
storage.get(Object.assign({}, DEFAULT_SETTINGS, { exclusionSelectors: getDefaultExclusionSelectors() }), (items) => {
593610
cacheStorage.get({ [LOCALE_CACHE_KEY]: null }, (cacheItems: { [LOCALE_CACHE_KEY]: Record<Exclude<LanguageCode, 'off'>, { source?: string; fetchedAt?: number }> | null }) => {
594-
latestLocaleMeta = extractLocaleMeta(cacheItems[LOCALE_CACHE_KEY])
611+
appState.latestLocaleMeta = extractLocaleMeta(cacheItems[LOCALE_CACHE_KEY])
595612
// Merge defaults with loaded items to ensure complete object
596613
const settings = normalizeSettings({ ...DEFAULT_SETTINGS, ...items })
597614
renderApp(settings)
@@ -602,7 +619,7 @@ export default function initOptionsPage() {
602619
chrome.storage.onChanged.addListener((changes, area) => {
603620
if (area !== 'sync' && area !== 'local') return
604621

605-
const newSettings = { ...currentSettings }
622+
const newSettings = { ...appState.currentSettings }
606623
let hasChange = false
607624

608625
// Update settings object with any changed values
@@ -637,17 +654,17 @@ export default function initOptionsPage() {
637654
// The options page listens for this to update the "JSON: Cloudflare" badge.
638655
if (changes[LOCALE_CACHE_KEY]) {
639656
const newValue = changes[LOCALE_CACHE_KEY].newValue
640-
latestLocaleMeta = extractLocaleMeta(newValue)
657+
appState.latestLocaleMeta = extractLocaleMeta(newValue)
641658

642659
// If we receive a NEW valid cache, we can clear the manual refresh state
643660
// and show the new source (e.g. Cloudflare).
644661
if (newValue && Object.keys(newValue).length > 0) {
645-
isManuallyRefreshing = false
662+
appState.isManuallyRefreshing = false
646663
}
647664

648665
const root = document.getElementById('root')
649-
if (root && !isManuallyRefreshing) {
650-
updateLocaleBadge(root, latestLocaleMeta, newSettings)
666+
if (root && !appState.isManuallyRefreshing) {
667+
updateLocaleBadge(root, appState.latestLocaleMeta, newSettings)
651668
}
652669
}
653670

verify_json.mjs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@
1111
import fs from 'fs';
1212
import path from 'path';
1313

14-
const files = [
15-
'src/locales/ja.json',
16-
'src/locales/zh-TW.json',
17-
'src/locales/zh-CN.json',
18-
'src/locales/ko.json',
19-
'src/locales/th.json',
20-
'src/locales/fr.json',
21-
'src/locales/it.json'
22-
];
14+
// Dynamically discover all .json files in src/locales/ and src/locales-extension/
15+
const targetDirs = ['src/locales', 'src/locales-extension'];
16+
const files = targetDirs.flatMap(dir => {
17+
const dirPath = path.join(process.cwd(), dir);
18+
if (!fs.existsSync(dirPath)) return [];
19+
return fs.readdirSync(dirPath)
20+
.filter(f => f.endsWith('.json'))
21+
.map(f => path.join(dir, f));
22+
});
2323

2424
let hasError = false;
2525

2626
// Regex to match valid JSON numbers (see ECMA-404)
27-
const JSON_NUMBER_REGEX = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/;
27+
const JSON_NUMBER_REGEX = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/;
2828

2929
/**
3030
* Lightweight JSON parser to detect duplicate keys per object while still
@@ -68,7 +68,8 @@ function verifyJsonStructure(source) {
6868
// Use JSON.parse to properly unescape
6969
JSON.parse(raw);
7070
} catch (e) {
71-
error(`Invalid string escape sequence in ${raw}: ${e?.message || e}`);
71+
const stringPos = index - start;
72+
error(`Invalid string escape sequence in ${raw}: ${e?.message || e} (at position ${stringPos} within the string, from JSON position ${start} to ${index})`);
7273
}
7374
advance(); // closing quote
7475
return raw;

web/Kumaflow-unpacked-install.zip

332 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)