From b51a1392b15994019b72cd5196bea8d64e0eec29 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 13 Oct 2026 15:44:15 +0200 Subject: [PATCH] fix(l10n): every Dutch key once, and a spec that keeps it so nl.json carried Flows, Reports and Store twice, nl.js fifteen keys. In nl.js the last copy won, so Set as default read "Instellen als standaard" and View mode "Weergave" while nl.json says "Als standaard instellen" and "Weergavemodus". The nl.json values stay. --- l10n/nl.js | 15 ------ l10n/nl.json | 3 -- src/__tests__/l10nNoDuplicateKeys.spec.js | 63 +++++++++++++++++++++++ 3 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 src/__tests__/l10nNoDuplicateKeys.spec.js diff --git a/l10n/nl.js b/l10n/nl.js index 43ed6929..1de4d5d2 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -541,7 +541,6 @@ OC.L10N.register( "e.g. /Documents/Marketing" : "bijv. /Documenten/Marketing", "Folder ID (optional, preferred)" : "Map-ID (optioneel, voorkeur)", "Numeric file id of the folder" : "Numeriek bestand-id van de map", - "View mode" : "Weergave", "Sort by" : "Sorteer op", "Sort descending" : "Aflopend sorteren", "Show thumbnails" : "Miniaturen tonen", @@ -990,7 +989,6 @@ OC.L10N.register( "No widgets match this filter" : "Geen widgets komen overeen met dit filter", "Catalog" : "Catalogus", "General" : "Algemeen", - "Group dashboards" : "Groepsdashboards", "Manage dashboards shared with a Nextcloud group. The “Default group” row controls the org-wide default dashboard set." : "Beheer dashboards die met een Nextcloud-groep worden gedeeld. De rij “Standaardgroep” regelt de organisatiebrede standaard dashboards.", "Loading groups" : "Groepen laden", "No groups configured" : "Geen groepen geconfigureerd", @@ -1003,26 +1001,18 @@ OC.L10N.register( "Failed to load group dashboards" : "Laden van groepsdashboards mislukt", "Failed to delete dashboard" : "Verwijderen van dashboard mislukt", "Last dashboard cannot be deleted" : "Het laatste dashboard kan niet worden verwijderd", - "Name" : "Naam", "Icon (Material Design name)" : "Pictogram (Material Design-naam)", "Layout template" : "Lay-outsjabloon", "Blank dashboard" : "Leeg dashboard", "KPI overview" : "KPI-overzicht", "People & teams" : "Mensen & teams", "Set as the group default dashboard" : "Instellen als standaard groepsdashboard", - "Cancel" : "Annuleren", - "Create" : "Aanmaken", - "Creating…" : "Bezig met aanmaken…", "Name must be at least 2 characters" : "Naam moet minstens 2 tekens bevatten", "Name must be at most 64 characters" : "Naam mag maximaal 64 tekens bevatten", "Manage dashboards for {group}" : "Dashboards beheren voor {group}", "Loading dashboards" : "Dashboards laden", "No dashboards in this group yet" : "Nog geen dashboards in deze groep", - "Default" : "Standaard", - "Set as default" : "Instellen als standaard", "Rename" : "Hernoemen", - "Delete" : "Verwijderen", - "Close" : "Sluiten", "New dashboard name" : "Nieuwe dashboardnaam", "Delete this dashboard? This cannot be undone." : "Dit dashboard verwijderen? Dit kan niet ongedaan worden gemaakt.", "Maximum dashboards per user" : "Maximum aantal dashboards per gebruiker", @@ -1060,7 +1050,6 @@ OC.L10N.register( "Waiting for content…" : "Wachten op inhoud…", "Spend analytics" : "Uitgavenanalyse", "View mode" : "Weergavemodus", - "Period" : "Periode", "Summary" : "Samenvatting", "Trend" : "Trend", "Top vendors" : "Belangrijkste leveranciers", @@ -1105,7 +1094,6 @@ OC.L10N.register( "Acknowledged" : "Bevestigd", "Pending" : "In afwachting", "Overdue" : "Te laat", - "User" : "Gebruiker", "Status" : "Status", "Acknowledged at" : "Bevestigd op", "Export CSV" : "CSV exporteren", @@ -1400,9 +1388,6 @@ OC.L10N.register( "Start page without navigation panel" : "Startpagina zonder navigatiepaneel", "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel." : "Als dit aanstaat heeft de dashboardweergave geen navigatiepaneel links en gebruikt hij de volle breedte. De dashboardkiezer en de bestemmingen Documentatie, Store, Rapporten, Functies en roadmap en instellingen staan dan in de knop Menu in de bovenste rij. Andere pagina's houden het paneel.", "Personal settings" : "Persoonlijke instellingen", - "Store" : "Store", - "Reports" : "Rapporten", - "Flows" : "Flows", "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings." : "Configureer je LaunchPad met groepvolgorde, demo-data, beheerdersrollen en voettekstinstellingen.", "Get your intranet started: configure groups, install demo data, and set up admin roles." : "Start je intranet: configureer groepen, installeer demo-data en stel beheerdersrollen in.", "A dashboard is shared with you" : "Er wordt een dashboard met je gedeeld", diff --git a/l10n/nl.json b/l10n/nl.json index 785b175e..beb49380 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -1377,9 +1377,6 @@ "Start page without navigation panel": "Startpagina zonder navigatiepaneel", "When on, the dashboard view has no left navigation panel and uses the full width. The dashboard switcher and the Documentation, Store, Reports, Features & roadmap and settings destinations are in the Menu button in the top row. Other pages keep the panel.": "Als dit aanstaat heeft de dashboardweergave geen navigatiepaneel links en gebruikt hij de volle breedte. De dashboardkiezer en de bestemmingen Documentatie, Store, Rapporten, Functies en roadmap en instellingen staan dan in de knop Menu in de bovenste rij. Andere pagina's houden het paneel.", "Personal settings": "Persoonlijke instellingen", - "Store": "Store", - "Reports": "Rapporten", - "Flows": "Flows", "Configure your LaunchPad instance with group ordering, demo data, admin roles, and footer settings.": "Configureer je LaunchPad met groepvolgorde, demo-data, beheerdersrollen en voettekstinstellingen.", "Get your intranet started: configure groups, install demo data, and set up admin roles.": "Start je intranet: configureer groepen, installeer demo-data en stel beheerdersrollen in.", "A dashboard is shared with you": "Er wordt een dashboard met je gedeeld", diff --git a/src/__tests__/l10nNoDuplicateKeys.spec.js b/src/__tests__/l10nNoDuplicateKeys.spec.js new file mode 100644 index 00000000..0854c435 --- /dev/null +++ b/src/__tests__/l10nNoDuplicateKeys.spec.js @@ -0,0 +1,63 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * A key appears once in the Dutch catalogues. Parallel pull requests each + * appended the same key; git merged both lines without a conflict, and the + * LAST copy won at runtime. In nl.js that made "Set as default" read + * "Instellen als standaard" and "View mode" read "Weergave", while nl.json + * and the rest of the app said otherwise. JSON.parse and a JS object cannot + * see a duplicate, so this reads the raw text. Approach copied from + * nextcloud-vue tests/l10n/catalogueNoDuplicateKeys.spec.js. + */ +import { readFileSync } from 'fs' +import { join } from 'path' +import { describe, expect, it } from 'vitest' + +const DIR = join(__dirname, '../../l10n') + +/** + * The keys that occur more than once in one object of a JSON-shaped text. + * + * @param {string} text The raw object text. + * @return {string[]} The duplicated keys. + */ +function duplicateKeys(text) { + const duplicates = [] + const seenStack = [new Set()] + const re = /"((?:[^"\\]|\\.)*)"\s*:|[{}]/g + let match + while ((match = re.exec(text)) !== null) { + if (match[0] === '{') { + seenStack.push(new Set()) + } else if (match[0] === '}') { + seenStack.pop() + } else { + const seen = seenStack[seenStack.length - 1] + if (seen.has(match[1])) { + duplicates.push(match[1]) + } + seen.add(match[1]) + } + } + return duplicates +} + +describe('Dutch l10n catalogues', () => { + it('nl.json has no key twice', () => { + const text = readFileSync(join(DIR, 'nl.json'), 'utf8') + JSON.parse(text) + expect(duplicateKeys(text)).toEqual([]) + }) + + it('nl.js has no key twice', () => { + const text = readFileSync(join(DIR, 'nl.js'), 'utf8') + // The translations object: from the first "{" to the "}" before the plural rule. + const body = text.slice(text.indexOf('{'), text.lastIndexOf('},')) + expect(duplicateKeys(body)).toEqual([]) + }) + + it('finds a duplicate when there is one', () => { + expect(duplicateKeys('{"a": "1", "b": "2", "a": "3"}')).toEqual(['a']) + }) +})