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_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
+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
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
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.
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
Binary file not shown.
+2
View File
@@ -20,6 +20,8 @@ REQUIRED_FILES = {
'popup.css',
'popup.html',
'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,
"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 @@
"<all_urls>"
],
"action": {
"default_title": "LinkLog",
"default_title": "__MSG_extensionName__",
"default_popup": "popup.html",
"default_icon": {
"16": "icon-16.png",
+21 -20
View File
@@ -5,65 +5,66 @@
<head>
<meta charset="utf-8" />
<title>LinkLog Settings</title>
<title data-i18n="settingsTitle">LinkLog Settings</title>
<link rel="stylesheet" href="options.css" />
</head>
<body>
<main class="options-shell">
<img class="extension-logo" src="logo.svg" alt="LinkLog" />
<h1>Settings</h1>
<img class="extension-logo" src="logo.svg" data-i18n-alt="extensionName" alt="LinkLog" />
<h1 data-i18n="settings">Settings</h1>
<div id="status" class="status hidden" aria-live="polite"></div>
<section id="logged-in" class="logged-in hidden" aria-live="polite">
<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>
<form id="settings-form">
<label>
Backend URL
<span data-i18n="backendUrlLabel">Backend URL</span>
<input id="backend-url" type="url" placeholder="https://example.com" />
</label>
<label>
Username
<input id="username" type="text" placeholder="alice" />
<span data-i18n="usernameLabel">Username</span>
<input id="username" type="text" data-i18n-placeholder="usernamePlaceholder" placeholder="alice" />
</label>
<label>
Password
<span data-i18n="passwordLabel">Password</span>
<input id="password" type="password" />
</label>
<button type="submit">Save and log in</button>
<button type="submit" data-i18n="saveAndLogIn">Save and log in</button>
</form>
<section class="about-plugin">
<h2>This Plugin</h2>
<p>This plugin will log links to a backend LinkLog server which will publish them on a website and potentially as
<h2 data-i18n="thisPlugin">This Plugin</h2>
<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
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>
<h2>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="accountRequirement">Users of this plugin will need an account on a LinkLog server</p>
<h2 data-i18n="privacy">Privacy</h2>
<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>
<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
with
the user's profile as registered on the backend server.
</p>
<h2>LinkLog Backend</h2>
<p>The backend URL is a selfhosted linklog application. See its <a
href="https://git.kolkman.org/olaf/Link-Log">repository</a> for its source.</p>
<h2 data-i18n="backendHeading">LinkLog Backend</h2>
<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" data-i18n="repository">repository</a> <span data-i18n="backendSource">for its source.</span></p>
</section>
</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="options.js"></script>
</body>
+8 -6
View File
@@ -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);
+14 -13
View File
@@ -4,48 +4,49 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>LinkLog</title>
<title data-i18n="extensionName">LinkLog</title>
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<main class="popup-shell">
<header>
<img class="extension-logo" src="logo.svg" alt="LinkLog" />
<a id="feed-link" class="feed-link hidden" href="/" target="_blank" rel="noopener noreferrer">LinkLog</a>
<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" data-i18n="extensionName">LinkLog</a>
</header>
<div id="status" class="status hidden" aria-live="polite"></div>
<form id="link-form">
<label>
Title
<span data-i18n="titleLabel">Title</span>
<input id="title" name="title" type="text" />
</label>
<label>
URL
<span data-i18n="urlLabel">URL</span>
<input id="url" name="url" type="url" />
</label>
<label>
Comment
<textarea id="comment" name="comment" rows="3" placeholder="Your comment"></textarea>
<span data-i18n="commentLabel">Comment</span>
<textarea id="comment" name="comment" rows="3" data-i18n-placeholder="commentPlaceholder" placeholder="Your comment"></textarea>
</label>
<fieldset>
<legend>Tags</legend>
<div id="existing-tags" class="tag-options">Loading tags...</div>
<input id="new-tags" type="text" pattern="#[^, ]+(,\s*#[^, ]+)*" placeholder="#new-tag, #another-tag" />
<legend data-i18n="tagsLabel">Tags</legend>
<div id="existing-tags" class="tag-options" data-i18n="loadingTags">Loading tags...</div>
<input id="new-tags" type="text" pattern="#[^, ]+(,\s*#[^, ]+)*" data-i18n-placeholder="newTagsPlaceholder" placeholder="#new-tag, #another-tag" />
</fieldset>
<div class="actions">
<button type="submit" id="submit-link">Save link</button>
<button type="button" id="open-settings" class="secondary">Settings</button>
<button type="submit" id="submit-link" data-i18n="saveLink">Save link</button>
<button type="button" id="open-settings" class="secondary" data-i18n="settings">Settings</button>
</div>
</form>
</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>
</body>
</html>
+10 -8
View File
@@ -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);
}
}