PageSourceSearch

https://invoicecraftly.com/_assets/js/features/localization/document-language-presentation.6f93947faf94.js

js invoicecraftly.com collected 2026-09-24 14:46:20 UTC 14,465 bytes, 289 lines download raw bytes

1import { LOCALE_AVAILABILITY, LOCALE_REGISTRY } from './locale-profiles.9915b2d28745.js';
2
3const DOCUMENT_COPY = Object.freeze({
4  en: Object.freeze({}),
5  da: Object.freeze({
6    Invoice: 'Faktura', Quote: 'Tilbud', Estimate: 'Prisoverslag', Receipt: 'Kvittering', 'Credit Note': 'Kreditnota',
7    Issued: 'Udstedt', Due: 'Forfalder', 'Valid until': 'Gyldig til', From: 'Fra', 'Bill To': 'Til',
8    'Received From': 'Modtaget fra', 'Credit To': 'Krediteres til', 'Prepared For': 'Til', Description: 'Beskrivelse',
9    Qty: 'Antal', 'Unit Price': 'Enhedspris', Tax: 'Moms', 'Sales tax': 'Moms', Amount: 'Beløb', Subtotal: 'Subtotal',
10    Discount: 'Rabat', Fees: 'Gebyrer', 'Quoted Total': 'Tilbud i alt', 'Estimated Total': 'Estimat i alt',
11    'Total Paid': 'Betalt i alt', 'Credit Total': 'Kredit i alt', 'Total Due': 'Beløb i alt', Terms: 'Betalingsbetingelser',
12    Account: 'Konto', Address: 'Adresse', Email: 'E-mail', Phone: 'Telefon', 'Tax ID': 'Skatte-ID', Delivery: 'Levering',
13    Place: 'Sted', 'Scan for contact': 'Scan for kontakt', 'Scan to pay': 'Scan for at betale',
14    'Made with InvoiceCraftly.com': 'Lavet med InvoiceCraftly.com', 'Due on Receipt': 'Betales ved modtagelse',
15    'Net 7': 'Netto 7 dage', 'Net 14': 'Netto 14 dage', 'Net 30': 'Netto 30 dage',
16    'Valid for 7 days': 'Gyldig i 7 dage', 'Valid for 14 days': 'Gyldig i 14 dage',
17    'Valid for 30 days': 'Gyldig i 30 dage', 'Valid until accepted': 'Gyldig indtil accept'
18  }),
19  nb: Object.freeze({
20    Invoice: 'Faktura', Quote: 'Tilbud', Estimate: 'Prisoverslag', Receipt: 'Kvittering', 'Credit Note': 'Kreditnota',
21    Issued: 'Utstedt', Due: 'Forfaller', 'Valid until': 'Gyldig til', From: 'Fra', 'Bill To': 'Til',
22    'Received From': 'Mottatt fra', 'Credit To': 'Krediteres til', 'Prepared For': 'Til', Description: 'Beskrivelse',
23    Qty: 'Antall', 'Unit Price': 'Enhetspris', Tax: 'MVA', 'Sales tax': 'MVA', Amount: 'Beløp', Subtotal: 'Delsum',
24    Discount: 'Rabatt', Fees: 'Gebyrer', 'Quoted Total': 'Tilbud totalt', 'Estimated Total': 'Estimert total',
25    'Total Paid': 'Betalt totalt', 'Credit Total': 'Kreditert totalt', 'Total Due': 'Til betaling', Terms: 'Betalingsbetingelser',
26    Account: 'Konto', Address: 'Adresse', Email: 'E-post', Phone: 'Telefon', 'Tax ID': 'Skatte-ID', Delivery: 'Levering',
27    Place: 'Sted', 'Scan for contact': 'Skann for kontakt', 'Scan to pay': 'Skann for å betale',
28    'Made with InvoiceCraftly.com': 'Laget med InvoiceCraftly.com', 'Due on Receipt': 'Betales ved mottak',
29    'Net 7': 'Netto 7 dager', 'Net 14': 'Netto 14 dager', 'Net 30': 'Netto 30 dager',
30    'Valid for 7 days': 'Gyldig i 7 dager', 'Valid for 14 days': 'Gyldig i 14 dager',
31    'Valid for 30 days': 'Gyldig i 30 dager', 'Valid until accepted': 'Gyldig til akseptert'
32  }),
33  sv: Object.freeze({
34    Invoice: 'Faktura', Quote: 'Offert', Estimate: 'Kostnadsförslag', Receipt: 'Kvitto', 'Credit Note': 'Kreditnota',
35    Issued: 'Utfärdad', Due: 'Förfaller', 'Valid until': 'Giltig till', From: 'Från', 'Bill To': 'Till',
36    'Received From': 'Mottaget från', 'Credit To': 'Krediteras till', 'Prepared For': 'Till', Description: 'Beskrivning',
37    Qty: 'Antal', 'Unit Price': 'Enhetspris', Tax: 'Moms', 'Sales tax': 'Moms', Amount: 'Belopp', Subtotal: 'Delsumma',
38    Discount: 'Rabatt', Fees: 'Avgifter', 'Quoted Total': 'Offert totalt', 'Estimated Total': 'Kostnadsförslag totalt',
39    'Total Paid': 'Betalt totalt', 'Credit Total': 'Krediterat totalt', 'Total Due': 'Att betala', Terms: 'Betalningsvillkor',
40    Account: 'Konto', Address: 'Adress', Email: 'E-post', Phone: 'Telefon', 'Tax ID': 'Organisations-/skattenummer', Delivery: 'Leverans',
41    Place: 'Plats', 'Scan for contact': 'Skanna för kontakt', 'Scan to pay': 'Skanna för betalning',
42    'Made with InvoiceCraftly.com': 'Skapad med InvoiceCraftly.com', 'Due on Receipt': 'Betalas vid mottagande',
43    'Net 7': 'Netto 7 dagar', 'Net 14': 'Netto 14 dagar', 'Net 30': 'Netto 30 dagar',
44    'Valid for 7 days': 'Giltig i 7 dagar', 'Valid for 14 days': 'Giltig i 14 dagar',
45    'Valid for 30 days': 'Giltig i 30 dagar', 'Valid until accepted': 'Giltig tills den accepteras'
46  }),
47  fi: Object.freeze({
48    Invoice: 'Lasku', Quote: 'Tarjous', Estimate: 'Kustannusarvio', Receipt: 'Kuitti', 'Credit Note': 'Hyvityslasku',
49    Issued: 'Päiväys', Due: 'Eräpäivä', 'Valid until': 'Voimassa asti', From: 'Myyjä', 'Bill To': 'Asiakas',
50    'Received From': 'Maksaja', 'Credit To': 'Hyvityksen saaja', 'Prepared For': 'Asiakas', Description: 'Kuvaus',
51    Qty: 'Määrä', 'Unit Price': 'Yksikköhinta', Tax: 'ALV', 'Sales tax': 'ALV', Amount: 'Summa', Subtotal: 'Välisumma',
52    Discount: 'Alennus', Fees: 'Lisämaksut', 'Quoted Total': 'Tarjous yhteensä', 'Estimated Total': 'Kustannusarvio yhteensä',
53    'Total Paid': 'Maksettu yhteensä', 'Credit Total': 'Hyvitys yhteensä', 'Total Due': 'Maksettava yhteensä', Terms: 'Maksuehdot',
54    Account: 'Tili', Address: 'Osoite', Email: 'Sähköposti', Phone: 'Puhelin', 'Tax ID': 'Y-tunnus / verotunniste', Delivery: 'Toimitus',
55    Place: 'Paikka', 'Scan for contact': 'Skannaa yhteystiedot', 'Scan to pay': 'Skannaa ja maksa',
56    'Made with InvoiceCraftly.com': 'Luotu InvoiceCraftly.com-palvelulla', 'Due on Receipt': 'Maksetaan heti',
57    'Net 7': 'Netto 7 päivää', 'Net 14': 'Netto 14 päivää', 'Net 30': 'Netto 30 päivää',
58    'Valid for 7 days': 'Voimassa 7 päivää', 'Valid for 14 days': 'Voimassa 14 päivää',
59    'Valid for 30 days': 'Voimassa 30 päivää', 'Valid until accepted': 'Voimassa hyväksymiseen asti'
60  })
61});
62
63const USER_CONTENT_SELECTOR = [
64  '.pv-wordmark',
65  '.pv-party-name',
66  '.pv-contact-text',
67  '.pv-notes',
68  '.pv-table tbody td'
69].join(', ');
70
71const DOCUMENT_PAGE_SELECTOR = '.page[data-document-locale]';
72const DOCUMENT_LANGUAGE_CONTROL_SELECTOR = '[data-path="localization.documentLocale"]';
73const VISUAL_EXPORT_TRIGGER_SELECTOR = '#btn-export, #btn-pdf, #btn-png, #btn-svg';
74
75function normalizeUiLocale(locale = 'en') {
76  const value = String(locale || 'en').toLowerCase();
77  if (value.startsWith('da')) return 'da';
78  if (value.startsWith('nb') || value.startsWith('no')) return 'nb';
79  if (value.startsWith('sv')) return 'sv';
80  if (value.startsWith('fi')) return 'fi';
81  return 'en';
82}
83
84function normalizeDocumentLocale(locale = 'en') {
85  const value = String(locale || 'en').toLowerCase();
86  if (value.startsWith('da')) return 'da';
87  if (value.startsWith('nb') || value.startsWith('no')) return 'nb';
88  if (value.startsWith('sv')) return 'sv';
89  if (value.startsWith('fi')) return 'fi';
90  return 'en';
91}
92
93function localeVisibleInPicker(locale, uiLocale) {
94  if (locale.availability === LOCALE_AVAILABILITY.ACTIVE) return true;
95  // A foundation locale is visible only inside its own isolated market preview.
96  // This lets Finland exercise native document rendering before public release
97  // without adding Finnish to every production editor language picker.
98  return locale.availability === LOCALE_AVAILABILITY.FOUNDATION && locale.id === uiLocale;
99}
100
101export function getDocumentLanguageOptions(uiLocale = 'en') {
102  const ui = normalizeUiLocale(uiLocale);
103  return Object.freeze(
104    Object.values(LOCALE_REGISTRY)
105      .filter((locale) => localeVisibleInPicker(locale, ui))
106      .map((locale) => Object.freeze({
107        value: locale.documentLocale,
108        label: locale.displayNames?.[ui] || locale.displayNames?.en || locale.documentLocale,
109        languageTag: locale.languageTag
110      }))
111  );
112}
113
114function activeDocumentLocale(locale, uiLocale = 'en') {
115  const normalized = normalizeDocumentLocale(locale);
116  return getDocumentLanguageOptions(uiLocale).some((option) => option.value === normalized) ? normalized : 'en';
117}
118
119function syncDocumentLanguageControl(control, selectedLocale = '') {
120  if (!control?.matches?.(DOCUMENT_LANGUAGE_CONTROL_SELECTOR)) return;
121  const uiLocale = normalizeUiLocale(document.documentElement.lang || 'en');
122  const options = getDocumentLanguageOptions(uiLocale);
123  const selected = activeDocumentLocale(selectedLocale || control.value || 'en', uiLocale);
124  const signature = `${uiLocale}:${options.map((option) => option.value).join(',')}`;
125  const currentValues = [...control.options].map((option) => option.value).join(',');
126  const desiredValues = options.map((option) => option.value).join(',');
127
128  if (control.dataset.documentLanguageOptions !== signature || currentValues !== desiredValues) {
129    const fragment = document.createDocumentFragment();
130    options.forEach((option) => {
131      const element = document.createElement('option');
132      element.value = option.value;
133      element.textContent = option.label;
134      element.lang = option.languageTag;
135      fragment.appendChild(element);
136    });
137    control.replaceChildren(fragment);
138    control.dataset.documentLanguageOptions = signature;
139  } else {
140    options.forEach((option, index) => {
141      const element = control.options[index];
142      if (!element) return;
143      element.textContent = option.label;
144      element.lang = option.languageTag;
145    });
146  }
147  control.value = selected;
148}
149
150function renderedDocumentLocale(root = document) {
151  const page = root.querySelector?.(DOCUMENT_PAGE_SELECTOR) || document.querySelector(DOCUMENT_PAGE_SELECTOR);
152  return page?.dataset?.documentLocale || '';
153}
154
155function syncDocumentLanguageControls(root = document, selectedLocale = '') {
156  const controls = [];
157  if (root?.matches?.(DOCUMENT_LANGUAGE_CONTROL_SELECTOR)) controls.push(root);
158  root?.querySelectorAll?.(DOCUMENT_LANGUAGE_CONTROL_SELECTOR).forEach((control) => controls.push(control));
159  const preferred = selectedLocale || renderedDocumentLocale(root);
160  controls.forEach((control) => syncDocumentLanguageControl(control, preferred));
161}
162
163export function getDocumentLanguage(invoice) {
164  return normalizeDocumentLocale(invoice?.localization?.documentLocale || 'en');
165}
166
167export function translateDocumentText(invoice, value) {
168  const text = String(value ?? '');
169  if (!text) return text;
170  const language = getDocumentLanguage(invoice);
171  return DOCUMENT_COPY[language]?.[text] || text;
172}
173
174function translateRenderedText(invoice, value) {
175  const leading = value.match(/^\s*/)?.[0] || '';
176  const trailing = value.match(/\s*$/)?.[0] || '';
177  const core = value.trim();
178  if (!core) return value;
179
180  const direct = translateDocumentText(invoice, core);
181  if (direct !== core) return `${leading}${direct}${trailing}`;
182
183  const colonLabel = core.match(/^([^:]+):$/);
184  if (colonLabel) {
185    const translated = translateDocumentText(invoice, colonLabel[1]);
186    if (translated !== colonLabel[1]) return `${leading}${translated}:${trailing}`;
187  }
188
189  for (const prefix of ['Issued', 'Due', 'Valid until']) {
190    if (core.startsWith(`${prefix} `)) {
191      return `${leading}${translateDocumentText(invoice, prefix)} ${core.slice(prefix.length + 1)}${trailing}`;
192    }
193  }
194
195  for (const prefix of ['Terms', 'Account']) {
196    if (core.startsWith(`${prefix}: `)) {
197      const remainder = core.slice(prefix.length + 2);
198      return `${leading}${translateDocumentText(invoice, prefix)}: ${translateDocumentText(invoice, remainder)}${trailing}`;
199    }
200  }
201
202  const taxBreakdown = core.match(/^(.+?) @ (.+)$/);
203  if (taxBreakdown) {
204    const translatedLabel = translateDocumentText(invoice, taxBreakdown[1]);
205    if (translatedLabel !== taxBreakdown[1]) return `${leading}${translatedLabel} @ ${taxBreakdown[2]}${trailing}`;
206  }
207
208  return value;
209}
210
211export function applyDocumentLanguagePresentation(root, invoice) {
212  if (!root || getDocumentLanguage(invoice) === 'en') return;
213  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
214  const textNodes = [];
215  while (walker.nextNode()) textNodes.push(walker.currentNode);
216  textNodes.forEach((node) => {
217    const element = node.parentElement;
218    if (!element || element.closest(USER_CONTENT_SELECTOR)) return;
219    const translated = translateRenderedText(invoice, node.nodeValue || '');
220    if (translated !== node.nodeValue) node.nodeValue = translated;
221  });
222
223  root.querySelectorAll('[aria-label], [title]').forEach((element) => {
224    ['aria-label', 'title'].forEach((attribute) => {
225      if (!element.hasAttribute(attribute)) return;
226      const value = element.getAttribute(attribute) || '';
227      const translated = translateRenderedText(invoice, value);
228      if (translated !== value) element.setAttribute(attribute, translated);
229    });
230  });
231}
232
233function applyToPage(page) {
234  if (!page?.matches?.(DOCUMENT_PAGE_SELECTOR)) return;
235  const documentLocale = page.dataset.documentLocale || 'en';
236  applyDocumentLanguagePresentation(page, {
237    localization: { documentLocale }
238  });
239  syncDocumentLanguageControls(document, documentLocale);
240}
241
242function applyWithin(node) {
243  if (node?.nodeType !== Node.ELEMENT_NODE) return;
244  if (node.matches(DOCUMENT_PAGE_SELECTOR)) applyToPage(node);
245  node.querySelectorAll?.(DOCUMENT_PAGE_SELECTOR).forEach(applyToPage);
246  const containingPage = node.closest?.(DOCUMENT_PAGE_SELECTOR);
247  if (containingPage) applyToPage(containingPage);
248  syncDocumentLanguageControls(node, renderedDocumentLocale());
249}
250
251function applyAllRenderedPages() {
252  document.querySelectorAll(DOCUMENT_PAGE_SELECTOR).forEach(applyToPage);
253  syncDocumentLanguageControls(document, renderedDocumentLocale());
254}
255
256/**
257 * Document language is a property of the document, not of the editor route.
258 * Install once for every editor so an English UI can preview/export a Danish,
259 * Norwegian or Swedish document and localized UI can still produce English.
260 * Foundation locales may render only inside their own isolated preview until
261 * promoted to ACTIVE.
262 *
263 * The language picker is also hydrated here from the locale registry so UI
264 * choices and renderer capability cannot drift apart as markets are added.
265 */
266export function installDocumentLanguagePresentation() {
267  applyAllRenderedPages();
268  const observer = new MutationObserver((mutations) => {
269    mutations.forEach((mutation) => {
270      if (mutation.type === 'attributes') {
271        applyWithin(mutation.target);
272        return;
273      }
274      mutation.addedNodes.forEach(applyWithin);
275    });
276  });
277  observer.observe(document.body, {
278    childList: true,
279    subtree: true,
280    attributes: true,
281    attributeFilter: ['data-document-locale']
282  });
283
284  document.addEventListener('click', (event) => {
285    if (event.target.closest?.(VISUAL_EXPORT_TRIGGER_SELECTOR)) applyAllRenderedPages();
286  }, { capture: true });
287
288  return observer;
289}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.