diff --git a/Makefile b/Makefile index f2fc82a..051b602 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ XPI_FILE := LinkLog-$(EXTENSION_VERSION).xpi XPI_UNSIGNED_DIR := XPI/unsigned XPI_SIGNED_DIR := XPI/signed XPI_OUTPUT := $(XPI_UNSIGNED_DIR)/$(XPI_FILE) -EXTENSION_FILES := manifest.json logo.svg icon-16.png icon-32.png icon-48.png icon-96.png options.css options.html options.js popup.css popup.html popup.js +EXTENSION_FILES := manifest.json logo.svg icon-16.png icon-32.png icon-48.png icon-96.png options.css options.html options.js popup.css popup.html popup.js l10n.js _locales/en-US/messages.json EXTENSION_SOURCES := $(addprefix webextension/,$(EXTENSION_FILES)) XPI_VALIDATOR := scripts/release/validate_xpi.py diff --git a/VIBE/CHAT_LOG.md b/VIBE/CHAT_LOG.md index f2003d0..7315aa5 100644 --- a/VIBE/CHAT_LOG.md +++ b/VIBE/CHAT_LOG.md @@ -232,6 +232,14 @@ On the home page, when clicking on the avatar or the user, show the profile info ### Assistant outcome Made each home-feed avatar and username link to the corresponding `//` profile page, added matching link layout styling, and added a regression assertion for the generated profile URL. +## 2026-08-26 + +### User +Localize the firefox plugin. + +### Assistant outcome +Added Firefox `browser.i18n` support with an `en-US` message catalog, localized popup and settings page labels and runtime messages, localized manifest metadata, and included the localization resources in XPI packaging and validation. + ### User Show the avatar with each entry on the home page. On the specific // page don't show the avatar and user name with each entry diff --git a/VIBE/PROMPTS.md b/VIBE/PROMPTS.md index adab5a5..25caeac 100644 --- a/VIBE/PROMPTS.md +++ b/VIBE/PROMPTS.md @@ -125,6 +125,7 @@ 116. When the user is authorized and on its // page show a Mastodon post button with logo; after posting keep it functional but change its color. 117. Continue to log interactions to the VIBE directory. 118. On the home page, when clicking on the avatar or the user, show the profile information. +119. Localize the firefox plugin. ## Future entries diff --git a/XPI/unsigned/LinkLog-0.1.0.xpi b/XPI/unsigned/LinkLog-0.1.0.xpi index 23cfaf0..d004638 100644 Binary files a/XPI/unsigned/LinkLog-0.1.0.xpi and b/XPI/unsigned/LinkLog-0.1.0.xpi differ diff --git a/scripts/release/validate_xpi.py b/scripts/release/validate_xpi.py index 1a87b51..170e7e1 100644 --- a/scripts/release/validate_xpi.py +++ b/scripts/release/validate_xpi.py @@ -20,6 +20,8 @@ REQUIRED_FILES = { 'popup.css', 'popup.html', 'popup.js', + 'l10n.js', + '_locales/en-US/messages.json', } diff --git a/webextension/_locales/en-US/messages.json b/webextension/_locales/en-US/messages.json new file mode 100644 index 0000000..4cb6096 --- /dev/null +++ b/webextension/_locales/en-US/messages.json @@ -0,0 +1,139 @@ +{ + "extensionName": { + "message": "LinkLog" + }, + "extensionDescription": { + "message": "Capture and submit page links to LinkLog." + }, + "settingsTitle": { + "message": "LinkLog Settings" + }, + "titleLabel": { + "message": "Title" + }, + "urlLabel": { + "message": "URL" + }, + "commentLabel": { + "message": "Comment" + }, + "commentPlaceholder": { + "message": "Your comment" + }, + "tagsLabel": { + "message": "Tags" + }, + "loadingTags": { + "message": "Loading tags..." + }, + "newTagsPlaceholder": { + "message": "#new-tag, #another-tag" + }, + "saveLink": { + "message": "Save link" + }, + "settings": { + "message": "Settings" + }, + "signOut": { + "message": "Sign out" + }, + "backendUrlLabel": { + "message": "Backend URL" + }, + "usernameLabel": { + "message": "Username" + }, + "usernamePlaceholder": { + "message": "alice" + }, + "passwordLabel": { + "message": "Password" + }, + "saveAndLogIn": { + "message": "Save and log in" + }, + "thisPlugin": { + "message": "This Plugin" + }, + "pluginDescription": { + "message": "This plugin will log links to a backend LinkLog server which will publish them on a website and potentially as an entry on a fediverse service or other locations, depending on the plugins configured." + }, + "accountRequirement": { + "message": "Users of this plugin will need an account on a LinkLog server" + }, + "privacy": { + "message": "Privacy" + }, + "privacyDescription": { + "message": "This plugin respects your privacy and does not collect any personal data beyond what is necessary to log links to your LinkLog server." + }, + "storedData": { + "message": "The URL - after a best effort try to remove tracking parameters -, the title of the webpage, a possible comment, and a timestamp of visit will be stored on the backend server, and will be made public in combination with the user's profile as registered on the backend server." + }, + "backendHeading": { + "message": "LinkLog Backend" + }, + "backendDescription": { + "message": "The backend URL is a selfhosted linklog application. See its" + }, + "backendSource": { + "message": "for its source." + }, + "copyright": { + "message": "Copyright © 2026 Olaf Kolkman" + }, + "repository": { + "message": "Repository" + }, + "signInToSelectTags": { + "message": "Sign in to select existing tags." + }, + "loadTagsFailed": { + "message": "Could not load existing tags." + }, + "submitting": { + "message": "Submitting..." + }, + "configureAndLogIn": { + "message": "Please configure the backend URL and log in first." + }, + "sessionExpired": { + "message": "Session expired. Re-authenticate in settings." + }, + "submissionFailed": { + "message": "Submission failed" + }, + "linkSaved": { + "message": "Link saved successfully" + }, + "submissionFailedConnection": { + "message": "Submission failed. Check your backend connection." + }, + "loggedInAt": { + "message": "$USERNAME$ logged in at $BACKEND$", + "placeholders": { + "username": { + "content": "$1" + }, + "backend": { + "content": "$2" + } + } + }, + "fillAllFields": { + "message": "Please fill in all fields" + }, + "loginFailed": { + "message": "Login failed" + }, + "loggedInSuccessfully": { + "message": "Logged in successfully" + }, + "unableToLogIn": { + "message": "Unable to log in. Check backend URL and credentials." + }, + "signedOut": { + "message": "Signed out" + } +} \ No newline at end of file diff --git a/webextension/l10n.js b/webextension/l10n.js new file mode 100644 index 0000000..4b3bec3 --- /dev/null +++ b/webextension/l10n.js @@ -0,0 +1,21 @@ +// Copyright © 2026 Olaf Kolkman +// SPDX-License-Identifier: GPL-3.0-or-later + +function getLocalizedMessage(messageId, substitutions = []) { + const message = browser.i18n.getMessage(messageId, substitutions); + return message || messageId; +} + +window.linklogI18n = getLocalizedMessage; + +document.querySelectorAll('[data-i18n]').forEach((element) => { + element.textContent = getLocalizedMessage(element.dataset.i18n); +}); + +document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => { + element.placeholder = getLocalizedMessage(element.dataset.i18nPlaceholder); +}); + +document.querySelectorAll('[data-i18n-alt]').forEach((element) => { + element.alt = getLocalizedMessage(element.dataset.i18nAlt); +}); \ No newline at end of file diff --git a/webextension/manifest.json b/webextension/manifest.json index 52d6c9d..e2d42b5 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -1,8 +1,9 @@ { "manifest_version": 3, - "name": "LinkLog", + "name": "__MSG_extensionName__", "version": "0.1.0", - "description": "Capture and submit page links to LinkLog.", + "description": "__MSG_extensionDescription__", + "default_locale": "en-US", "permissions": [ "activeTab", "storage", @@ -12,7 +13,7 @@ "" ], "action": { - "default_title": "LinkLog", + "default_title": "__MSG_extensionName__", "default_popup": "popup.html", "default_icon": { "16": "icon-16.png", diff --git a/webextension/options.html b/webextension/options.html index ed66068..513f48f 100644 --- a/webextension/options.html +++ b/webextension/options.html @@ -5,65 +5,66 @@ - LinkLog Settings + LinkLog Settings
- -

Settings

+ +

Settings

- +
-

This Plugin

-

This plugin will log links to a backend LinkLog server which will publish them on a website and potentially as +

This Plugin

+

This plugin will log links to a backend LinkLog server which will publish them on a website and potentially as an entry on a fediverse service or other locations, depending on the plugins configured.

-

Users of this plugin will need an account on a LinkLog server

-

Privacy

-

This plugin respects your privacy and does not collect any personal data beyond what is necessary to log links +

Users of this plugin will need an account on a LinkLog server

+

Privacy

+

This plugin respects your privacy and does not collect any personal data beyond what is necessary to log links to your LinkLog server.

-

The URL - after a best effort try to remove tracking parameters -, the title of the webpage, a possible +

The URL - after a best effort try to remove tracking parameters -, the title of the webpage, a possible comment, and a timestamp of visit will be stored on the backend server, and will be made public in combination with the user's profile as registered on the backend server.

-

LinkLog Backend

-

The backend URL is a selfhosted linklog application. See its repository for its source.

+

LinkLog Backend

+

The backend URL is a selfhosted linklog application. See its repository for its source.

- + + diff --git a/webextension/options.js b/webextension/options.js index c1f4cce..a5ec85a 100644 --- a/webextension/options.js +++ b/webextension/options.js @@ -12,6 +12,8 @@ const session = document.getElementById('logged-in'); const sessionSummary = document.getElementById('session-summary'); const signOutButton = document.getElementById('sign-out'); +const t = window.linklogI18n; + function setStatus(message, isError = false) { statusEl.textContent = message; statusEl.classList.remove('hidden'); @@ -43,7 +45,7 @@ async function loadSettings() { } function showLoggedIn(username, backendUrl) { - sessionSummary.textContent = `${username} logged in at ${backendUrl}`; + sessionSummary.textContent = t('loggedInAt', [username, backendUrl]); session.classList.remove('hidden'); form.classList.add('hidden'); } @@ -64,7 +66,7 @@ async function saveSettingsAndLogin(event) { const password = passwordInput.value; if (!backendUrl || !username || !password) { - setStatus('Please fill in all fields', true); + setStatus(t('fillAllFields'), true); return; } @@ -76,7 +78,7 @@ async function saveSettingsAndLogin(event) { }); if (!response.ok) { - throw new Error('Login failed'); + throw new Error(t('loginFailed')); } const data = await response.json(); @@ -91,9 +93,9 @@ async function saveSettingsAndLogin(event) { showLoggedIn(data.user?.username || username, backendUrl); passwordInput.value = ''; - setStatus('Logged in successfully'); + setStatus(t('loggedInSuccessfully')); } catch (error) { - setStatus('Unable to log in. Check backend URL and credentials.', true); + setStatus(t('unableToLogIn'), true); } } @@ -108,7 +110,7 @@ async function signOut() { } await clearSession(); showLoggedOut(); - setStatus('Signed out'); + setStatus(t('signedOut')); } form.addEventListener('submit', saveSettingsAndLogin); diff --git a/webextension/popup.html b/webextension/popup.html index 253d5ba..a5d59b0 100644 --- a/webextension/popup.html +++ b/webextension/popup.html @@ -4,48 +4,49 @@ - LinkLog + LinkLog
- - + +
- + + diff --git a/webextension/popup.js b/webextension/popup.js index e19600d..2ae82a4 100644 --- a/webextension/popup.js +++ b/webextension/popup.js @@ -11,6 +11,8 @@ const existingTags = document.getElementById('existing-tags'); const newTagsInput = document.getElementById('new-tags'); const feedLink = document.getElementById('feed-link'); +const t = window.linklogI18n; + function setStatus(message, isError = false) { statusEl.textContent = message; statusEl.classList.remove('hidden'); @@ -42,14 +44,14 @@ async function updateFeedLink() { async function loadExistingTags() { const settings = await getSettings(); if (!settings.backendUrl || !settings.accessToken) { - existingTags.textContent = 'Sign in to select existing tags.'; + existingTags.textContent = t('signInToSelectTags'); return; } const response = await fetch(`${settings.backendUrl}/api/tags`, { headers: {'Authorization': `Bearer ${settings.accessToken}`}, }); if (!response.ok) { - existingTags.textContent = 'Could not load existing tags.'; + existingTags.textContent = t('loadTagsFailed'); return; } const tags = await response.json(); @@ -96,14 +98,14 @@ async function populateCurrentTab() { async function handleSubmit(event) { event.preventDefault(); - setStatus('Submitting...', false); + setStatus(t('submitting'), false); const settings = await getSettings(); const token = settings.accessToken; const backendUrl = settings.backendUrl; if (!token || !backendUrl) { - setStatus('Please configure the backend URL and log in first.', true); + setStatus(t('configureAndLogIn'), true); browser.runtime.openOptionsPage(); return; } @@ -125,18 +127,18 @@ async function handleSubmit(event) { }); if (response.status === 401) { - setStatus('Session expired. Re-authenticate in settings.', true); + setStatus(t('sessionExpired'), true); browser.runtime.openOptionsPage(); return; } if (!response.ok) { - throw new Error('Submission failed'); + throw new Error(t('submissionFailed')); } - setStatus('Link saved successfully'); + setStatus(t('linkSaved')); } catch (error) { - setStatus('Submission failed. Check your backend connection.', true); + setStatus(t('submissionFailedConnection'), true); } }