Added Locales to the webextention
Build LinkLog Development Image / development-image (push) Successful in 9s

This commit is contained in:
2026-08-26 12:14:30 +02:00
parent f0ae526a96
commit 3fdf146498
12 changed files with 229 additions and 51 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ XPI_FILE := LinkLog-$(EXTENSION_VERSION).xpi
XPI_UNSIGNED_DIR := XPI/unsigned XPI_UNSIGNED_DIR := XPI/unsigned
XPI_SIGNED_DIR := XPI/signed XPI_SIGNED_DIR := XPI/signed
XPI_OUTPUT := $(XPI_UNSIGNED_DIR)/$(XPI_FILE) 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)) EXTENSION_SOURCES := $(addprefix webextension/,$(EXTENSION_FILES))
XPI_VALIDATOR := scripts/release/validate_xpi.py XPI_VALIDATOR := scripts/release/validate_xpi.py
+8
View File
@@ -232,6 +232,14 @@ On the home page, when clicking on the avatar or the user, show the profile info
### Assistant outcome ### Assistant outcome
Made each home-feed avatar and username link to the corresponding `/<username>/` profile page, added matching link layout styling, and added a regression assertion for the generated profile URL. Made each home-feed avatar and username link to the corresponding `/<username>/` 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 ### User
Show the avatar with each entry on the home page. On the specific /<user>/ page don't show the avatar and user name with each entry Show the avatar with each entry on the home page. On the specific /<user>/ page don't show the avatar and user name with each entry
+1
View File
@@ -125,6 +125,7 @@
116. When the user is authorized and on its /<user>/ page show a Mastodon post button with logo; after posting keep it functional but change its color. 116. When the user is authorized and on its /<user>/ 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. 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. 118. On the home page, when clicking on the avatar or the user, show the profile information.
119. Localize the firefox plugin.
## Future entries ## Future entries
Binary file not shown.
+2
View File
@@ -20,6 +20,8 @@ REQUIRED_FILES = {
'popup.css', 'popup.css',
'popup.html', 'popup.html',
'popup.js', 'popup.js',
'l10n.js',
'_locales/en-US/messages.json',
} }
+139
View File
@@ -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"
}
}
+21
View File
@@ -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);
});
+4 -3
View File
@@ -1,8 +1,9 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "LinkLog", "name": "__MSG_extensionName__",
"version": "0.1.0", "version": "0.1.0",
"description": "Capture and submit page links to LinkLog.", "description": "__MSG_extensionDescription__",
"default_locale": "en-US",
"permissions": [ "permissions": [
"activeTab", "activeTab",
"storage", "storage",
@@ -12,7 +13,7 @@
"<all_urls>" "<all_urls>"
], ],
"action": { "action": {
"default_title": "LinkLog", "default_title": "__MSG_extensionName__",
"default_popup": "popup.html", "default_popup": "popup.html",
"default_icon": { "default_icon": {
"16": "icon-16.png", "16": "icon-16.png",
+21 -20
View File
@@ -5,65 +5,66 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>LinkLog Settings</title> <title data-i18n="settingsTitle">LinkLog Settings</title>
<link rel="stylesheet" href="options.css" /> <link rel="stylesheet" href="options.css" />
</head> </head>
<body> <body>
<main class="options-shell"> <main class="options-shell">
<img class="extension-logo" src="logo.svg" alt="LinkLog" /> <img class="extension-logo" src="logo.svg" data-i18n-alt="extensionName" alt="LinkLog" />
<h1>Settings</h1> <h1 data-i18n="settings">Settings</h1>
<div id="status" class="status hidden" aria-live="polite"></div> <div id="status" class="status hidden" aria-live="polite"></div>
<section id="logged-in" class="logged-in hidden" aria-live="polite"> <section id="logged-in" class="logged-in hidden" aria-live="polite">
<p id="session-summary"></p> <p id="session-summary"></p>
<button type="button" id="sign-out">Sign out</button> <button type="button" id="sign-out" data-i18n="signOut">Sign out</button>
</section> </section>
<form id="settings-form"> <form id="settings-form">
<label> <label>
Backend URL <span data-i18n="backendUrlLabel">Backend URL</span>
<input id="backend-url" type="url" placeholder="https://example.com" /> <input id="backend-url" type="url" placeholder="https://example.com" />
</label> </label>
<label> <label>
Username <span data-i18n="usernameLabel">Username</span>
<input id="username" type="text" placeholder="alice" /> <input id="username" type="text" data-i18n-placeholder="usernamePlaceholder" placeholder="alice" />
</label> </label>
<label> <label>
Password <span data-i18n="passwordLabel">Password</span>
<input id="password" type="password" /> <input id="password" type="password" />
</label> </label>
<button type="submit">Save and log in</button> <button type="submit" data-i18n="saveAndLogIn">Save and log in</button>
</form> </form>
<section class="about-plugin"> <section class="about-plugin">
<h2>This Plugin</h2> <h2 data-i18n="thisPlugin">This Plugin</h2>
<p>This plugin will log links to a backend LinkLog server which will publish them on a website and potentially as <p data-i18n="pluginDescription">This plugin will log links to a backend LinkLog server which will publish them on a website and potentially as
an entry an entry
on a fediverse service or other locations, depending on the plugins configured.</p> on a fediverse service or other locations, depending on the plugins configured.</p>
<p>Users of this plugin will need an account on a LinkLog server</p> <p data-i18n="accountRequirement">Users of this plugin will need an account on a LinkLog server</p>
<h2>Privacy</h2> <h2 data-i18n="privacy">Privacy</h2>
<p>This plugin respects your privacy and does not collect any personal data beyond what is necessary to log links <p data-i18n="privacyDescription">This plugin respects your privacy and does not collect any personal data beyond what is necessary to log links
to your LinkLog server.</p> to your LinkLog server.</p>
<p>The URL - after a best effort try to remove tracking parameters -, the title of the webpage, a possible <p data-i18n="storedData">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 comment, and a timestamp of visit will be stored on the backend server, and will be made public in combination
with with
the user's profile as registered on the backend server. the user's profile as registered on the backend server.
</p> </p>
<h2>LinkLog Backend</h2> <h2 data-i18n="backendHeading">LinkLog Backend</h2>
<p>The backend URL is a selfhosted linklog application. See its <a <p><span data-i18n="backendDescription">The backend URL is a selfhosted linklog application. See its</span> <a
href="https://git.kolkman.org/olaf/Link-Log">repository</a> for its source.</p> href="https://git.kolkman.org/olaf/Link-Log" data-i18n="repository">repository</a> <span data-i18n="backendSource">for its source.</span></p>
</section> </section>
</main> </main>
<footer class="extension-footer">Copyright © 2026 Olaf Kolkman · <a <footer class="extension-footer"><span data-i18n="copyright">Copyright © 2026 Olaf Kolkman</span> · <a
href="https://git.kolkman.org/olaf/Link-Log">Repository</a></footer> href="https://git.kolkman.org/olaf/Link-Log" data-i18n="repository">Repository</a></footer>
<script src="l10n.js"></script>
<script src="options.js"></script> <script src="options.js"></script>
</body> </body>
+8 -6
View File
@@ -12,6 +12,8 @@ const session = document.getElementById('logged-in');
const sessionSummary = document.getElementById('session-summary'); const sessionSummary = document.getElementById('session-summary');
const signOutButton = document.getElementById('sign-out'); const signOutButton = document.getElementById('sign-out');
const t = window.linklogI18n;
function setStatus(message, isError = false) { function setStatus(message, isError = false) {
statusEl.textContent = message; statusEl.textContent = message;
statusEl.classList.remove('hidden'); statusEl.classList.remove('hidden');
@@ -43,7 +45,7 @@ async function loadSettings() {
} }
function showLoggedIn(username, backendUrl) { function showLoggedIn(username, backendUrl) {
sessionSummary.textContent = `${username} logged in at ${backendUrl}`; sessionSummary.textContent = t('loggedInAt', [username, backendUrl]);
session.classList.remove('hidden'); session.classList.remove('hidden');
form.classList.add('hidden'); form.classList.add('hidden');
} }
@@ -64,7 +66,7 @@ async function saveSettingsAndLogin(event) {
const password = passwordInput.value; const password = passwordInput.value;
if (!backendUrl || !username || !password) { if (!backendUrl || !username || !password) {
setStatus('Please fill in all fields', true); setStatus(t('fillAllFields'), true);
return; return;
} }
@@ -76,7 +78,7 @@ async function saveSettingsAndLogin(event) {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Login failed'); throw new Error(t('loginFailed'));
} }
const data = await response.json(); const data = await response.json();
@@ -91,9 +93,9 @@ async function saveSettingsAndLogin(event) {
showLoggedIn(data.user?.username || username, backendUrl); showLoggedIn(data.user?.username || username, backendUrl);
passwordInput.value = ''; passwordInput.value = '';
setStatus('Logged in successfully'); setStatus(t('loggedInSuccessfully'));
} catch (error) { } 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(); await clearSession();
showLoggedOut(); showLoggedOut();
setStatus('Signed out'); setStatus(t('signedOut'));
} }
form.addEventListener('submit', saveSettingsAndLogin); form.addEventListener('submit', saveSettingsAndLogin);
+14 -13
View File
@@ -4,48 +4,49 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>LinkLog</title> <title data-i18n="extensionName">LinkLog</title>
<link rel="stylesheet" href="popup.css" /> <link rel="stylesheet" href="popup.css" />
</head> </head>
<body> <body>
<main class="popup-shell"> <main class="popup-shell">
<header> <header>
<img class="extension-logo" src="logo.svg" alt="LinkLog" /> <img class="extension-logo" src="logo.svg" data-i18n-alt="extensionName" alt="LinkLog" />
<a id="feed-link" class="feed-link hidden" href="/" target="_blank" rel="noopener noreferrer">LinkLog</a> <a id="feed-link" class="feed-link hidden" href="/" target="_blank" rel="noopener noreferrer" data-i18n="extensionName">LinkLog</a>
</header> </header>
<div id="status" class="status hidden" aria-live="polite"></div> <div id="status" class="status hidden" aria-live="polite"></div>
<form id="link-form"> <form id="link-form">
<label> <label>
Title <span data-i18n="titleLabel">Title</span>
<input id="title" name="title" type="text" /> <input id="title" name="title" type="text" />
</label> </label>
<label> <label>
URL <span data-i18n="urlLabel">URL</span>
<input id="url" name="url" type="url" /> <input id="url" name="url" type="url" />
</label> </label>
<label> <label>
Comment <span data-i18n="commentLabel">Comment</span>
<textarea id="comment" name="comment" rows="3" placeholder="Your comment"></textarea> <textarea id="comment" name="comment" rows="3" data-i18n-placeholder="commentPlaceholder" placeholder="Your comment"></textarea>
</label> </label>
<fieldset> <fieldset>
<legend>Tags</legend> <legend data-i18n="tagsLabel">Tags</legend>
<div id="existing-tags" class="tag-options">Loading tags...</div> <div id="existing-tags" class="tag-options" data-i18n="loadingTags">Loading tags...</div>
<input id="new-tags" type="text" pattern="#[^, ]+(,\s*#[^, ]+)*" placeholder="#new-tag, #another-tag" /> <input id="new-tags" type="text" pattern="#[^, ]+(,\s*#[^, ]+)*" data-i18n-placeholder="newTagsPlaceholder" placeholder="#new-tag, #another-tag" />
</fieldset> </fieldset>
<div class="actions"> <div class="actions">
<button type="submit" id="submit-link">Save link</button> <button type="submit" id="submit-link" data-i18n="saveLink">Save link</button>
<button type="button" id="open-settings" class="secondary">Settings</button> <button type="button" id="open-settings" class="secondary" data-i18n="settings">Settings</button>
</div> </div>
</form> </form>
</main> </main>
<footer class="extension-footer">Copyright © 2026 Olaf Kolkman · <a href="https://git.kolkman.org/olaf/Link-Log">Repository</a></footer> <footer class="extension-footer"><span data-i18n="copyright">Copyright © 2026 Olaf Kolkman</span> · <a href="https://git.kolkman.org/olaf/Link-Log" data-i18n="repository">Repository</a></footer>
<script src="l10n.js"></script>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
</html> </html>
+10 -8
View File
@@ -11,6 +11,8 @@ const existingTags = document.getElementById('existing-tags');
const newTagsInput = document.getElementById('new-tags'); const newTagsInput = document.getElementById('new-tags');
const feedLink = document.getElementById('feed-link'); const feedLink = document.getElementById('feed-link');
const t = window.linklogI18n;
function setStatus(message, isError = false) { function setStatus(message, isError = false) {
statusEl.textContent = message; statusEl.textContent = message;
statusEl.classList.remove('hidden'); statusEl.classList.remove('hidden');
@@ -42,14 +44,14 @@ async function updateFeedLink() {
async function loadExistingTags() { async function loadExistingTags() {
const settings = await getSettings(); const settings = await getSettings();
if (!settings.backendUrl || !settings.accessToken) { if (!settings.backendUrl || !settings.accessToken) {
existingTags.textContent = 'Sign in to select existing tags.'; existingTags.textContent = t('signInToSelectTags');
return; return;
} }
const response = await fetch(`${settings.backendUrl}/api/tags`, { const response = await fetch(`${settings.backendUrl}/api/tags`, {
headers: {'Authorization': `Bearer ${settings.accessToken}`}, headers: {'Authorization': `Bearer ${settings.accessToken}`},
}); });
if (!response.ok) { if (!response.ok) {
existingTags.textContent = 'Could not load existing tags.'; existingTags.textContent = t('loadTagsFailed');
return; return;
} }
const tags = await response.json(); const tags = await response.json();
@@ -96,14 +98,14 @@ async function populateCurrentTab() {
async function handleSubmit(event) { async function handleSubmit(event) {
event.preventDefault(); event.preventDefault();
setStatus('Submitting...', false); setStatus(t('submitting'), false);
const settings = await getSettings(); const settings = await getSettings();
const token = settings.accessToken; const token = settings.accessToken;
const backendUrl = settings.backendUrl; const backendUrl = settings.backendUrl;
if (!token || !backendUrl) { if (!token || !backendUrl) {
setStatus('Please configure the backend URL and log in first.', true); setStatus(t('configureAndLogIn'), true);
browser.runtime.openOptionsPage(); browser.runtime.openOptionsPage();
return; return;
} }
@@ -125,18 +127,18 @@ async function handleSubmit(event) {
}); });
if (response.status === 401) { if (response.status === 401) {
setStatus('Session expired. Re-authenticate in settings.', true); setStatus(t('sessionExpired'), true);
browser.runtime.openOptionsPage(); browser.runtime.openOptionsPage();
return; return;
} }
if (!response.ok) { if (!response.ok) {
throw new Error('Submission failed'); throw new Error(t('submissionFailed'));
} }
setStatus('Link saved successfully'); setStatus(t('linkSaved'));
} catch (error) { } catch (error) {
setStatus('Submission failed. Check your backend connection.', true); setStatus(t('submissionFailedConnection'), true);
} }
} }