Skip to content

Commit f94bd9b

Browse files
authored
Merge pull request #933 from nextcloud-libraries/feat/universal-environment
feat: add support for universal environment
2 parents 53660ba + d4557e1 commit f94bd9b

16 files changed

Lines changed: 344 additions & 253 deletions

lib/date.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { getCanonicalLocale } from './locale.ts'
88

9-
declare let window: Nextcloud.v29.WindowWithGlobals
9+
declare let globalThis: Nextcloud.v29.WindowWithGlobals
1010

1111
export type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
1212

@@ -17,8 +17,8 @@ export type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
1717
*/
1818
export function getFirstDay(): WeekDay {
1919
// Server rendered
20-
if (typeof window.firstDay !== 'undefined') {
21-
return window.firstDay as WeekDay
20+
if (typeof globalThis.firstDay !== 'undefined') {
21+
return globalThis.firstDay as WeekDay
2222
}
2323

2424
// Try to fallback to Intl
@@ -47,8 +47,8 @@ export function getFirstDay(): WeekDay {
4747
*/
4848
export function getDayNames(): string[] {
4949
// Server rendered
50-
if (typeof window.dayNames !== 'undefined') {
51-
return window.dayNames
50+
if (typeof globalThis.dayNames !== 'undefined') {
51+
return globalThis.dayNames
5252
}
5353

5454
// Fallback to Intl
@@ -68,8 +68,8 @@ export function getDayNames(): string[] {
6868
* Get a list of day names (short names)
6969
*/
7070
export function getDayNamesShort(): string[] {
71-
if (typeof window.dayNamesShort !== 'undefined') {
72-
return window.dayNamesShort
71+
if (typeof globalThis.dayNamesShort !== 'undefined') {
72+
return globalThis.dayNamesShort
7373
}
7474

7575
// Fallback to Intl
@@ -91,8 +91,8 @@ export function getDayNamesShort(): string[] {
9191
*/
9292
export function getDayNamesMin(): string[] {
9393
// Server rendered
94-
if (typeof window.dayNamesMin !== 'undefined') {
95-
return window.dayNamesMin
94+
if (typeof globalThis.dayNamesMin !== 'undefined') {
95+
return globalThis.dayNamesMin
9696
}
9797

9898
// Fallback to Intl
@@ -113,8 +113,8 @@ export function getDayNamesMin(): string[] {
113113
*/
114114
export function getMonthNames(): string[] {
115115
// Server rendered
116-
if (typeof window.monthNames !== 'undefined') {
117-
return window.monthNames
116+
if (typeof globalThis.monthNames !== 'undefined') {
117+
return globalThis.monthNames
118118
}
119119

120120
// Fallback to Intl
@@ -140,8 +140,8 @@ export function getMonthNames(): string[] {
140140
*/
141141
export function getMonthNamesShort(): string[] {
142142
// Server rendered
143-
if (typeof window.monthNamesShort !== 'undefined') {
144-
return window.monthNamesShort
143+
if (typeof globalThis.monthNamesShort !== 'undefined') {
144+
return globalThis.monthNamesShort
145145
}
146146

147147
// Fallback to Intl

lib/globals.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
/* eslint-disable camelcase */
7+
8+
import type { PluralFunction, Translations } from './registry.ts'
9+
10+
declare global {
11+
var _nc_l10n_locale: string
12+
var _nc_l10n_language: string
13+
14+
var _oc_l10n_registry_translations: Record<string, Translations>
15+
var _oc_l10n_registry_plural_functions: Record<string, PluralFunction>
16+
}
17+
18+
export {}

lib/locale.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*/
55

6-
const environmentLocale = Intl.DateTimeFormat().resolvedOptions().locale
7-
86
/**
97
* Returns the user's locale
108
*/
119
export function getLocale(): string {
12-
return document.documentElement.dataset.locale || environmentLocale.replaceAll(/-/g, '_')
10+
return globalThis._nc_l10n_locale
1311
}
1412

1513
/**
@@ -20,11 +18,43 @@ export function getCanonicalLocale(): string {
2018
return getLocale().replaceAll(/_/g, '-')
2119
}
2220

21+
/**
22+
* Set the current user's language (locally).
23+
* This is to be used only for e.g. usage within web workers etc.
24+
*
25+
* @param locale - The new language code
26+
* @since 3.4.0
27+
*/
28+
export function setLocale(locale: string): void {
29+
globalThis._nc_l10n_locale = locale
30+
31+
// also for browsers set the DOM
32+
if (typeof document !== 'undefined') {
33+
document.documentElement.dataset.locale = locale
34+
}
35+
}
36+
2337
/**
2438
* Returns the user's language
2539
*/
2640
export function getLanguage(): string {
27-
return document.documentElement.lang || navigator.language
41+
return globalThis._nc_l10n_language
42+
}
43+
44+
/**
45+
* Set the current user's language (locally).
46+
* This is to be used only for e.g. usage within web workers etc.
47+
*
48+
* @param lang - The new language code
49+
* @since 3.4.0
50+
*/
51+
export function setLanguage(lang: string): void {
52+
globalThis._nc_l10n_language = lang
53+
54+
// also for browsers set the DOM
55+
if (typeof document !== 'undefined') {
56+
document.documentElement.lang = lang
57+
}
2858
}
2959

3060
/**
@@ -68,3 +98,11 @@ export function isRTL(language?: string): boolean {
6898

6999
return rtlLanguages.includes(languageCode)
70100
}
101+
102+
// Initialize global state if needed (e.g. when not in DOM context like on WebWorker)
103+
104+
globalThis._nc_l10n_locale ??= (typeof document !== 'undefined' && document.documentElement.dataset.locale)
105+
|| Intl.DateTimeFormat().resolvedOptions().locale.replaceAll(/-/g, '_')
106+
107+
globalThis._nc_l10n_language ??= (typeof document !== 'undefined' && document.documentElement.lang)
108+
|| (globalThis.navigator?.language ?? 'en')

lib/registry.ts

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// <reference types="@nextcloud/typings" />
21
/*!
32
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
43
* SPDX-License-Identifier: GPL-3.0-or-later
@@ -32,19 +31,6 @@ export type Translations = Record<string, string | string[] | undefined>
3231
*/
3332
export type PluralFunction = (number: number) => number
3433

35-
/**
36-
* Extended window interface with translation registry
37-
* Exported just for internal testing purpose
38-
*
39-
* @private
40-
*/
41-
export interface NextcloudWindowWithRegistry extends Nextcloud.v27.WindowWithGlobals {
42-
_oc_l10n_registry_translations?: Record<string, Translations>
43-
_oc_l10n_registry_plural_functions?: Record<string, PluralFunction>
44-
}
45-
46-
declare const window: NextcloudWindowWithRegistry
47-
4834
export interface AppTranslations {
4935
translations: Translations
5036
pluralFunction: PluralFunction
@@ -54,12 +40,11 @@ export interface AppTranslations {
5440
* Check if translations and plural function are set for given app
5541
*
5642
* @param appId - The app id
57-
* @return
5843
*/
59-
export function hasAppTranslations(appId: string) {
44+
export function hasAppTranslations(appId: string): boolean {
6045
return (
61-
window._oc_l10n_registry_translations?.[appId] !== undefined
62-
&& window._oc_l10n_registry_plural_functions?.[appId] !== undefined
46+
appId in globalThis._oc_l10n_registry_translations
47+
&& appId in globalThis._oc_l10n_registry_plural_functions
6348
)
6449
}
6550

@@ -74,34 +59,27 @@ export function registerAppTranslations(
7459
appId: string,
7560
translations: Translations,
7661
pluralFunction: PluralFunction,
77-
) {
62+
): void {
7863
if (appId === '__proto__' || appId === 'constructor' || appId === 'prototype') {
7964
throw new Error('Invalid appId')
8065
}
8166

82-
window._oc_l10n_registry_translations = Object.assign(
83-
window._oc_l10n_registry_translations || {},
84-
{
85-
[appId]: Object.assign(window._oc_l10n_registry_translations?.[appId] || {}, translations),
86-
},
87-
)
67+
globalThis._oc_l10n_registry_translations[appId] = {
68+
...(globalThis._oc_l10n_registry_translations[appId] || {}),
69+
...translations,
70+
}
8871

89-
window._oc_l10n_registry_plural_functions = Object.assign(
90-
window._oc_l10n_registry_plural_functions || {},
91-
{
92-
[appId]: pluralFunction,
93-
},
94-
)
72+
globalThis._oc_l10n_registry_plural_functions[appId] = pluralFunction
9573
}
9674

9775
/**
9876
* Unregister all translations and plural function for given app
9977
*
10078
* @param appId - The app id
10179
*/
102-
export function unregisterAppTranslations(appId: string) {
103-
delete window._oc_l10n_registry_translations?.[appId]
104-
delete window._oc_l10n_registry_plural_functions?.[appId]
80+
export function unregisterAppTranslations(appId: string): void {
81+
delete globalThis._oc_l10n_registry_translations[appId]
82+
delete globalThis._oc_l10n_registry_plural_functions[appId]
10583
}
10684

10785
/**
@@ -111,7 +89,11 @@ export function unregisterAppTranslations(appId: string) {
11189
*/
11290
export function getAppTranslations(appId: string): AppTranslations {
11391
return {
114-
translations: window._oc_l10n_registry_translations?.[appId] ?? {},
115-
pluralFunction: window._oc_l10n_registry_plural_functions?.[appId] ?? ((number: number) => number),
92+
translations: globalThis._oc_l10n_registry_translations[appId] ?? {},
93+
pluralFunction: globalThis._oc_l10n_registry_plural_functions[appId] ?? ((number: number) => number),
11694
}
11795
}
96+
97+
// Initialize global state if needed
98+
globalThis._oc_l10n_registry_translations ??= {}
99+
globalThis._oc_l10n_registry_plural_functions ??= {}

lib/translation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function translate<T extends string>(app: string, text: T, placeholders?:
6363
* @param optionsOrNumber - The translation options or a number to replace `%n` with
6464
* @param options - Options object
6565
* @param options.escape - Enable/disable auto escape of placeholders (by default enabled)
66-
* @param options.sanitize - Enable/disable sanitization (by default enabled)
66+
* @param options.sanitize - Enable/disable sanitization (by default enabled) [WARNING: This only work in DOM environment!]
6767
*/
6868
export function translate<T extends string>(
6969
app: string,
@@ -92,7 +92,7 @@ export function translate<T extends string>(
9292
}
9393

9494
const identity = <T>(value: T): T => value
95-
const optSanitize = allOptions.sanitize ? DOMPurify.sanitize : identity
95+
const optSanitize = (allOptions.sanitize ? DOMPurify.sanitize : identity) || identity
9696
const optEscape = allOptions.escape ? escapeHTML : identity
9797

9898
const isValidReplacement = (value: unknown) => typeof value === 'string' || typeof value === 'number'

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
"lint": "eslint .",
4343
"lint:fix": "eslint --fix lib tests",
4444
"prerelease:format-changelog": "node build/format-changelog.mjs",
45-
"test": "LANG=en-US vitest run",
45+
"test": "npm run test:dom && npm run test:node",
4646
"test:coverage": "LANG=en-US vitest run --coverage",
47+
"test:dom": "LANG=en-US vitest run",
48+
"test:node": "LANG=en-US vitest run --environment node",
4749
"test:watch": "LANG=en-US vitest watch"
4850
},
4951
"browserslist": [

0 commit comments

Comments
 (0)