@sv443-network/userutils 8.3.2 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,15 +5,25 @@
5
5
  Lightweight library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and more.
6
6
 
7
7
  Contains builtin TypeScript declarations. Supports ESM and CJS imports via a bundler and global declaration via `@require`
8
+
9
+ You may want to check out my [template for userscripts in TypeScript](https://github.com/Sv443/Userscript.ts) that you can use to get started quickly. It also includes this library by default.
8
10
  If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
9
11
 
10
12
  <br>
13
+
14
+ [![minified bundle size badge](https://badgen.net/bundlephobia/min/@sv443-network/userutils)](https://bundlephobia.com/package/@sv443-network/userutils)
15
+ [![minified and gzipped bundle size badge](https://badgen.net/bundlephobia/minzip/@sv443-network/userutils)](https://bundlephobia.com/package/@sv443-network/userutils)
16
+ [![tree shaking support badge](https://badgen.net/bundlephobia/tree-shaking/@sv443-network/userutils)](https://bundlephobia.com/package/@sv443-network/userutils)
17
+
18
+ [![github stargazers badge](https://badgen.net/github/stars/Sv443-Network/UserUtils?icon=github)](https://github.com/Sv443-Network/UserUtils/stargazers)
19
+ [![discord server badge](https://badgen.net/discord/online-members/aBH4uRG?icon=discord)](https://dc.sv443.net/)
20
+
11
21
  <sup>
12
22
  View the documentation of previous major releases:
13
23
  </sup>
14
24
  <sub>
15
25
 
16
- <a href="https://github.com/Sv443-Network/UserUtils/blob/v7.3.0/README.md" rel="noopener noreferrer">7.3.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v6.3.0/README.md" rel="noopener noreferrer">6.3.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v5.0.1/README.md" rel="noopener noreferrer">5.0.1</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v4.2.1/README.md" rel="noopener noreferrer">4.2.1</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v3.0.0/README.md" rel="noopener noreferrer">3.0.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v2.0.1/README.md" rel="noopener noreferrer">2.0.1</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v1.2.0/README.md" rel="noopener noreferrer">1.2.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v0.5.3/README.md" rel="noopener noreferrer">0.5.3</a>
26
+ <a href="https://github.com/Sv443-Network/UserUtils/blob/v8.0.0/README.md" rel="noopener noreferrer">8.0.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v7.0.0/README.md" rel="noopener noreferrer">7.0.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v6.0.0/README.md" rel="noopener noreferrer">6.0.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v5.0.0/README.md" rel="noopener noreferrer">5.0.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v4.0.0/README.md" rel="noopener noreferrer">4.0.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v3.0.0/README.md" rel="noopener noreferrer">3.0.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v2.0.0/README.md" rel="noopener noreferrer">2.0.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v1.0.0/README.md" rel="noopener noreferrer">1.0.0</a>, <a href="https://github.com/Sv443-Network/UserUtils/blob/v0.5.3/README.md" rel="noopener noreferrer">0.5.3</a>
17
27
 
18
28
  </sub>
19
29
  </div>
@@ -42,6 +52,7 @@ View the documentation of previous major releases:
42
52
  - [`clamp()`](#clamp) - constrain a number between a min and max value
43
53
  - [`mapRange()`](#maprange) - map a number from one range to the same spot in another range
44
54
  - [`randRange()`](#randrange) - generate a random number between a min and max boundary
55
+ - [`digitCount()`](#digitcount) - calculate the amount of digits in a number
45
56
  - [**Misc:**](#misc)
46
57
  - [`DataStore`](#datastore) - class that manages a hybrid sync & async persistent JSON database, including data migration
47
58
  - [`DataStoreSerializer`](#datastoreserializer) - class for importing & exporting data of multiple DataStore instances, including compression, checksumming and running migrations
@@ -56,14 +67,16 @@ View the documentation of previous major releases:
56
67
  - [`decompress()`](#decompress) - decompress a previously compressed string
57
68
  - [`computeHash()`](#computehash) - compute the hash / checksum of a string or ArrayBuffer
58
69
  - [`randomId()`](#randomid) - generate a random ID of a given length and radix
70
+ - [`consumeGen()`](#consumegen) - consumes a ValueGen and returns the value
71
+ - [`consumeStringGen()`](#consumestringgen) - consumes a StringGen and returns the string
59
72
  - [**Arrays:**](#arrays)
60
73
  - [`randomItem()`](#randomitem) - returns a random item from an array
61
74
  - [`randomItemIndex()`](#randomitemindex) - returns a tuple of a random item and its index from an array
62
75
  - [`takeRandomItem()`](#takerandomitem) - returns a random item from an array and mutates it to remove the item
63
76
  - [`randomizeArray()`](#randomizearray) - returns a copy of the array with its items in a random order
64
77
  - [**Translation:**](#translation)
65
- - [`tr()`](#tr) - simple translation of a string to another language
66
- - [`tr.forLang()`](#trforlang) - specify a language besides the currently set one for the translation
78
+ - [`tr()`](#tr) - simple JSON-based translation system with placeholder and nesting support
79
+ - [`tr.forLang()`](#trforlang) - translate with the specified language instead of the currently active one
67
80
  - [`tr.addLanguage()`](#traddlanguage) - add a language and its translations
68
81
  - [`tr.setLanguage()`](#trsetlanguage) - set the currently active language for translations
69
82
  - [`tr.getLanguage()`](#trgetlanguage) - returns the currently active language
@@ -79,6 +92,8 @@ View the documentation of previous major releases:
79
92
  - [`NonEmptyString`](#nonemptystring) - any string that should have at least one character
80
93
  - [`LooseUnion`](#looseunion) - a union that gives autocomplete in the IDE but also allows any other value of the same type
81
94
  - [`Prettify`](#prettify) - expands a complex type into a more readable format while keeping functionality the same
95
+ - [`ValueGen`](#valuegen) - a "generator" value that allows for super flexible value typing and declaration
96
+ - [`StringGen`](#stringgen) - a "generator" string that allows for super flexible string typing and declaration, including enhanced support for unions
82
97
 
83
98
  <br><br>
84
99
 
@@ -86,12 +101,16 @@ View the documentation of previous major releases:
86
101
  ## Installation:
87
102
  Shameless plug: I made a [template for userscripts in TypeScript](https://github.com/Sv443/Userscript.ts) that you can use to get started quickly. It also includes this library by default.
88
103
 
89
- - If you are using a bundler (like webpack, rollup, vite, etc.), you can install this package using npm:
104
+ - If you are using a bundler (like webpack, rollup, vite, etc.), you can install this package in one of the following ways:
90
105
  ```
91
106
  npm i @sv443-network/userutils
107
+ pnpm i @sv443-network/userutils
108
+ yarn add @sv443-network/userutils
109
+ npx jsr install @sv443-network/userutils
110
+ deno add jsr:@sv443-network/userutils
92
111
  ```
93
- <sup>For other package managers, check out the respective install command on the [JavaScript Registry](https://jsr.io/@sv443-network/userutils)</sup>
94
- Then, import it in your script as usual:
112
+ Then import it in your script as usual:
113
+
95
114
  ```ts
96
115
  import { addGlobalStyle } from "@sv443-network/userutils";
97
116
 
@@ -102,16 +121,26 @@ Shameless plug: I made a [template for userscripts in TypeScript](https://github
102
121
 
103
122
  <br>
104
123
 
105
- - If you are not using a bundler or want to reduce the size of your userscript, you can include the latest release by adding one of these directives to the userscript header, depending on your preferred CDN:
124
+ - If you are not using a bundler, want to reduce the size of your userscript, or declared the package as external in your bundler, you can include the latest release by adding one of these directives to the userscript header, depending on your preferred CDN:
125
+
126
+ Versioned (recommended):
106
127
  ```
107
- // @require https://greasyfork.org/scripts/472956-userutils/code/UserUtils.js
128
+ // @require https://cdn.jsdelivr.net/npm/@sv443-network/userutils@INSERT_VERSION/dist/index.global.js
129
+ // @require https://unpkg.com/@sv443-network/userutils@INSERT_VERSION/dist/index.global.js
108
130
  ```
131
+ Non-versioned (not recommended because auto-updating):
109
132
  ```
133
+ // @require https://update.greasyfork.org/scripts/472956/UserUtils.js
110
134
  // @require https://openuserjs.org/src/libs/Sv443/UserUtils.js
111
135
  ```
112
- (in order for your userscript not to break on a major library update, instead use the versioned URL at the top of the [GreasyFork page](https://greasyfork.org/scripts/472956-userutils))
113
136
 
114
- Then, access the functions on the global variable `UserUtils`:
137
+ > [!NOTE]
138
+ > In order for your userscript not to break on a major library update, use one the versioned URLs above after replacing `INSERT_VERSION` with the desired version (e.g. `8.3.2`) or the versioned URL that's shown at the top of the [GreasyFork page.](https://greasyfork.org/scripts/472956-userutils)
139
+
140
+ <br>
141
+
142
+ - Then, access the functions on the global variable `UserUtils`:
143
+
115
144
  ```ts
116
145
  UserUtils.addGlobalStyle("body { background-color: red; }");
117
146
 
@@ -120,10 +149,27 @@ Shameless plug: I made a [template for userscripts in TypeScript](https://github
120
149
  const { clamp } = UserUtils;
121
150
  console.log(clamp(1, 5, 10)); // 5
122
151
  ```
123
- If you're using TypeScript and it complains about the missing global variable `UserUtils`, install the library using the package manager of your choice and add the following inside a `.d.ts` file somewhere in your project:
152
+
153
+ <br>
154
+
155
+ - If you're using TypeScript and it complains about the missing global variable `UserUtils`, install the library using the package manager of your choice and add the following inside a `.d.ts` file somewhere in the directory (or a subdirectory) defined in your `tsconfig.json`'s `baseUrl` option or `include` array:
156
+
124
157
  ```ts
158
+ declare const UserUtils: typeof import("@sv443-network/userutils");
159
+
125
160
  declare global {
126
- const UserUtils: typeof import("@sv443-network/userutils");
161
+ interface Window {
162
+ UserUtils: typeof UserUtils;
163
+ }
164
+ }
165
+ ```
166
+
167
+ <br>
168
+
169
+ - If you're using a linter like ESLint, it might complain about the global variable `UserUtils` not being defined. To fix this, add the following to your ESLint configuration file:
170
+ ```json
171
+ "globals": {
172
+ "UserUtils": "readonly"
127
173
  }
128
174
  ```
129
175
 
@@ -137,8 +183,11 @@ Each feature has example code that can be expanded by clicking on the text "Exam
137
183
  The usages and examples are written in TypeScript and use ESM import syntax, but the library can also be used in plain JavaScript after removing the type annotations (and changing the imports if you are using CommonJS or the global declaration).
138
184
  If the usage section contains multiple usages of the function, each occurrence represents an overload and you can choose which one you want to use.
139
185
 
140
- Some features require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other requirements.
141
- Their documentation will contain a section marked by a warning emoji (⚠️) that will go into more detail.
186
+ Some features require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other specific requirements and limitations.
187
+ Those will be listed in a section marked by a warning emoji (⚠️) each.
188
+
189
+ If you need help with something, please [create a new discussion](https://github.com/Sv443-Network/UserUtils/discussions) or [join my Discord server.](https://dc.sv443.net/)
190
+ For submitting bug reports or feature requests, please use the [GitHub issue tracker.](https://github.com/Sv443-Network/UserUtils/issues)
142
191
 
143
192
  <br><br>
144
193
 
@@ -908,9 +957,11 @@ setInnerHtmlUnsafe(myXssElement, userModifiableVariable);
908
957
  Usage:
909
958
  ```ts
910
959
  clamp(num: number, min: number, max: number): number
960
+ clamp(num: number, max: number): number
911
961
  ```
912
962
 
913
963
  Clamps a number between a min and max boundary (inclusive).
964
+ If only the `num` and `max` arguments are passed, the `min` boundary will be set to 0.
914
965
 
915
966
  <details><summary><b>Example - click to view</b></summary>
916
967
 
@@ -918,13 +969,14 @@ Clamps a number between a min and max boundary (inclusive).
918
969
  import { clamp } from "@sv443-network/userutils";
919
970
 
920
971
  clamp(7, 0, 10); // 7
921
- clamp(-1, 0, 10); // 0
972
+ clamp(7, 10); // 7 (equivalent to the above)
973
+ clamp(-1, 10); // 0
922
974
  clamp(5, -5, 0); // 0
923
975
  clamp(99999, 0, 10); // 10
924
976
 
925
- // clamp without a min or max boundary:
926
- clamp(-99999, -Infinity, 0); // -99999
927
- clamp(99999, 0, Infinity); // 99999
977
+ // use Infinity to clamp without a min or max boundary:
978
+ clamp(Number.MAX_SAFE_INTEGER, Infinity); // 9007199254740991
979
+ clamp(Number.MIN_SAFE_INTEGER, -Infinity, 0); // -9007199254740991
928
980
  ```
929
981
  </details>
930
982
 
@@ -998,6 +1050,40 @@ benchmark(true); // Generated 100k in 461ms
998
1050
  ```
999
1051
  </details>
1000
1052
 
1053
+ <br>
1054
+
1055
+ ### digitCount()
1056
+ Usage:
1057
+ ```ts
1058
+ digitCount(num: number | Stringifiable): number
1059
+ ```
1060
+
1061
+ Calculates and returns the amount of digits in the given number.
1062
+ The given value will be converted by being passed to `String()` and then `Number()` before the calculation.
1063
+ Returns `NaN` if the number is invalid.
1064
+
1065
+ <details><summary><b>Example - click to view</b></summary>
1066
+
1067
+ ```ts
1068
+ import { digitCount } from "@sv443-network/userutils";
1069
+
1070
+ const num1 = 123;
1071
+ const num2 = 123456789;
1072
+ const num3 = " 123456789 ";
1073
+ const num4 = Number.MAX_SAFE_INTEGER;
1074
+ const num5 = "a123b456c789d";
1075
+ const num6 = parseInt("0x123456789abcdef", 16);
1076
+
1077
+ digitCount(num1); // 3
1078
+ digitCount(num2); // 9
1079
+ digitCount(num3); // 9
1080
+ digitCount(num4); // 16
1081
+ digitCount(num5); // NaN (because hex conversion has to be done through parseInt(str, 16)), like below:
1082
+ digitCount(num6); // 17
1083
+ ```
1084
+
1085
+ </details>
1086
+
1001
1087
  <br><br>
1002
1088
 
1003
1089
  <!-- #region Misc -->
@@ -1684,15 +1770,15 @@ If an array or NodeList is passed, the amount of contained items will be used.
1684
1770
  ```ts
1685
1771
  import { autoPlural } from "@sv443-network/userutils";
1686
1772
 
1687
- autoPlural("apple", 0); // "apples"
1688
- autoPlural("apple", 1); // "apple"
1689
- autoPlural("apple", 2); // "apples"
1773
+ autoPlural("item", 0); // "items"
1774
+ autoPlural("item", 1); // "item"
1775
+ autoPlural("item", 2); // "items"
1690
1776
 
1691
- autoPlural("apple", [1]); // "apple"
1692
- autoPlural("apple", [1, 2]); // "apples"
1777
+ autoPlural("element", document.querySelectorAll("html")); // "element"
1778
+ autoPlural("element", document.querySelectorAll("*")); // "elements"
1693
1779
 
1694
1780
  const items = [1, 2, 3, 4, "foo", "bar"];
1695
- console.log(`Found ${items.length} ${autoPlural("item", items)}`); // "Found 6 items"
1781
+ console.log(items.length, autoPlural("item", items)); // "6 items"
1696
1782
  ```
1697
1783
  </details>
1698
1784
 
@@ -1993,6 +2079,85 @@ benchmark(true, true); // Generated 10k in 1054ms
1993
2079
 
1994
2080
  <br><br>
1995
2081
 
2082
+ ### consumeGen()
2083
+ Usage:
2084
+ ```ts
2085
+ consumeGen(valGen: ValueGen):
2086
+ ```
2087
+
2088
+ Turns a [`ValueGen`](#valuegen) into its final value.
2089
+ ValueGen allows for tons of flexibility in how the value can be obtained. Calling this function will resolve the final value.
2090
+
2091
+ <details><summary><b>Example - click to view</b></summary>
2092
+
2093
+ ```ts
2094
+ import { consumeGen, type ValueGen } from "@sv443-network/userutils";
2095
+
2096
+ async function doSomething(value: ValueGen<number>) {
2097
+ // type gets inferred as `number` because above `value` is typed as a ValueGen<number>
2098
+ const finalValue = await consumeGen(value);
2099
+ console.log(finalValue);
2100
+ }
2101
+
2102
+ // the following are all valid and yield 42:
2103
+ doSomething(42);
2104
+ doSomething(() => 42);
2105
+ doSomething(Promise.resolve(42));
2106
+ doSomething(async () => 42);
2107
+
2108
+ // throws a typescript error:
2109
+ doSomething("foo");
2110
+ ```
2111
+
2112
+ </details>
2113
+
2114
+ <br><br>
2115
+
2116
+ ### consumeStringGen()
2117
+ Usage:
2118
+ ```ts
2119
+ consumeStringGen(strGen: StringGen): Promise<string>
2120
+ ```
2121
+
2122
+ Turns a [`StringGen`](#stringgen) into its final string value.
2123
+ StringGen allows for tons of flexibility in how the string can be obtained. Calling this function will resolve the final string.
2124
+ Optionally you can use the template parameter to define the union of strings that the StringGen should yield.
2125
+
2126
+ <details><summary><b>Example - click to view</b></summary>
2127
+
2128
+ ```ts
2129
+ import { consumeStringGen, type StringGen } from "@sv443-network/userutils";
2130
+
2131
+ export class MyTextPromptThing {
2132
+ // full flexibility on how the string can be passed to the constructor,
2133
+ // because it can be obtained synchronously or asynchronously,
2134
+ // in string or function form:
2135
+ constructor(private text: StringGen) {}
2136
+
2137
+ /** Shows the prompt dialog */
2138
+ public async showPrompt() {
2139
+ const promptText = await consumeStringGen(this.text);
2140
+ const promptHtml = promptText.trim().replace(/\n/g, "<br>");
2141
+
2142
+ // ...
2143
+ }
2144
+ }
2145
+
2146
+ // all valid:
2147
+ const myText = "Hello, World!";
2148
+ new MyTextPromptThing(myText);
2149
+ new MyTextPromptThing(() => myText);
2150
+ new MyTextPromptThing(Promise.resolve(myText));
2151
+ new MyTextPromptThing(async () => myText);
2152
+
2153
+ // throws a typescript error:
2154
+ new MyTextPromptThing(420);
2155
+ ```
2156
+
2157
+ </details>
2158
+
2159
+ <br><br>
2160
+
1996
2161
  <!-- #region Arrays -->
1997
2162
  ## Arrays:
1998
2163
 
@@ -2100,43 +2265,63 @@ Pluralization is not supported but can be achieved manually by adding variations
2100
2265
  ### tr()
2101
2266
  Usage:
2102
2267
  ```ts
2103
- tr(key: string, ...values: Stringifiable[]): string
2268
+ tr(key: string, ...insertValues: Stringifiable[]): string
2104
2269
  ```
2105
2270
 
2106
2271
  The function returns the translation of the passed key in the language added by [`tr.addLanguage()`](#traddlanguage) and set by [`tr.setLanguage()`](#trsetlanguage)
2107
- Should the translation contain placeholders in the format `%n`, where `n` is the number of the value starting at 1, they will be replaced with the respective item of the `values` rest parameter.
2108
- The items of the `values` rest parameter will be stringified using `toString()` (see [Stringifiable](#stringifiable)) before being inserted into the translation.
2272
+ Should the translation contain placeholders in the format `%n`, where `n` is the number of the value starting at 1, they will be replaced with the respective item of the `insertValues` rest parameter.
2273
+ The items of the `insertValues` rest parameter will be stringified using `toString()` (see [Stringifiable](#stringifiable)) before being inserted into the translation.
2274
+
2275
+ Should you be using nested objects in your translations, you can use the dot notation to access them.
2276
+ First, the key will be split by dots and the parts will be used to traverse the translation object.
2277
+ If that doesn't yield a result, the function will try to access the key including dots on the top level of the translation object.
2278
+ If that also doesn't yield a result, the key itself will be returned.
2279
+
2280
+ If no language has been added or set before calling this function, it will also return the key itself.
2109
2281
 
2110
- If the key is not found or no language has been added or set before calling this function, it will return the key itself.
2111
- If the key is found and the translation contains placeholders but no values are passed, it will return the translation as-is, including unmodified placeholders.
2112
- If the key is found, the translation doesn't contain placeholders but values are still passed, they will be ignored and the translation will be returned as-is.
2282
+ To check if a translation has been found, compare the returned value with the key. If they are the same, the translation was not found.
2283
+ You could also write a wrapper function that can then return a default value or `null` if the translation was not found instead.
2284
+
2285
+ If the key is found and the translation contains placeholders but none or an insufficient amount of values are passed, it will try to insert as many values as were passed and leave the rest of the placeholders untouched in their `%n` format.
2286
+ If the key is found, the translation doesn't contain placeholders but values are still passed, the values will be ignored and the translation will be returned without modification.
2113
2287
 
2114
2288
  <details><summary><b>Example - click to view</b></summary>
2115
2289
 
2116
2290
  ```ts
2117
2291
  import { tr } from "@sv443-network/userutils";
2118
2292
 
2293
+ // add languages and translations:
2119
2294
  tr.addLanguage("en", {
2120
- "welcome": "Welcome",
2121
- "welcome_name": "Welcome, %1",
2295
+ welcome: {
2296
+ generic: "Welcome",
2297
+ with_name: "Welcome, %1",
2298
+ },
2122
2299
  });
2300
+
2123
2301
  tr.addLanguage("de", {
2124
- "welcome": "Willkommen",
2125
- "welcome_name": "Willkommen, %1",
2302
+ welcome: {
2303
+ generic: "Willkommen",
2304
+ with_name: "Willkommen, %1",
2305
+ },
2126
2306
  });
2127
2307
 
2128
2308
  // this has to be called at least once before calling tr()
2129
2309
  tr.setLanguage("en");
2130
2310
 
2131
- console.log(tr("welcome")); // "Welcome"
2132
- console.log(tr("welcome_name", "John")); // "Welcome, John"
2133
- console.log(tr("non_existent_key")); // "non_existent_key"
2311
+ console.log(tr("welcome.generic")); // "Welcome"
2312
+ console.log(tr("welcome.with_name", "John")); // "Welcome, John"
2313
+
2314
+ console.log(tr("non_existent_key")); // "non_existent_key"
2315
+ console.log(tr("welcome")); // "welcome" (because anything that isn't a string will make the function return the key itself)
2134
2316
 
2135
2317
  // language can be changed at any time, synchronously
2136
2318
  tr.setLanguage("de");
2137
2319
 
2138
- console.log(tr("welcome")); // "Willkommen"
2139
- console.log(tr("welcome_name", "John")); // "Willkommen, John"
2320
+ console.log(tr("welcome.generic")); // "Willkommen"
2321
+
2322
+ // or without overwriting the current language:
2323
+
2324
+ console.log(tr.forLang("en", "welcome.generic")); // "Welcome"
2140
2325
  ```
2141
2326
  </details>
2142
2327
 
@@ -2145,7 +2330,7 @@ console.log(tr("welcome_name", "John")); // "Willkommen, John"
2145
2330
  ### tr.forLang()
2146
2331
  Usage:
2147
2332
  ```ts
2148
- tr.forLang(language: string, key: string, ...values: Stringifiable[]): string
2333
+ tr.forLang(language: string, key: string, ...insertValues: Stringifiable[]): string
2149
2334
  ```
2150
2335
 
2151
2336
  Returns the translation of the passed key in the specified language. Otherwise behaves exactly like [`tr()`](#tr)
@@ -2167,8 +2352,8 @@ tr.addLanguage("de", {
2167
2352
  // the language is set to "en"
2168
2353
  tr.setLanguage("en");
2169
2354
 
2170
- console.log(tr("welcome_name", "John")); // "Welcome"
2171
- // the language doesn't need to be changed:
2355
+ console.log(tr("welcome_name", "John")); // "Welcome, John"
2356
+ // no need to call tr.setLanguage():
2172
2357
  console.log(tr.forLang("de", "welcome_name", "John")); // "Willkommen, John"
2173
2358
  ```
2174
2359
  </details>
@@ -2178,12 +2363,12 @@ console.log(tr.forLang("de", "welcome_name", "John")); // "Willkommen, John"
2178
2363
  ### tr.addLanguage()
2179
2364
  Usage:
2180
2365
  ```ts
2181
- tr.addLanguage(language: string, translations: Record<string, string>): void
2366
+ tr.addLanguage(language: string, translations: Record<string, string | object>): void
2182
2367
  ```
2183
2368
 
2184
- Adds a language and its associated translations to the translation function.
2185
- The passed language can be any unique identifier, though I recommend sticking to the [ISO 639-1 standard.](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
2186
- The passed translations should be an object where the key is the translation key used in `tr()` and the value is the translation itself.
2369
+ Adds or overwrites a language and its associated translations to the translation function.
2370
+ The passed language can be any unique identifier, though I highly recommend sticking to a standard like [BCP 47 / RFC 5646](https://www.rfc-editor.org/rfc/rfc5646.txt) (which is used by the [`Intl` namespace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) and methods like [`Number.toLocaleString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString)), or [ISO 639-1.](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
2371
+ The passed translations can either be a flat object where the key is the translation key used in `tr()` and the value is the translation itself, or an infinitely nestable object structure containing the same.
2187
2372
  If `tr.addLanguage()` is called multiple times with the same language, the previous translations of that language will be overwritten.
2188
2373
 
2189
2374
  The translation values may contain placeholders in the format `%n`, where `n` is the number of the value starting at 1.
@@ -2192,54 +2377,63 @@ These can be used to inject values into the translation when calling `tr()`
2192
2377
  <details><summary><b>Example - click to view</b></summary>
2193
2378
 
2194
2379
  ```ts
2195
- import { tr } from "@sv443-network/userutils";
2380
+ import { tr, type Stringifiable } from "@sv443-network/userutils";
2196
2381
 
2197
2382
  // add a language with associated translations:
2198
2383
 
2199
- tr.addLanguage("de", {
2200
- "color": "Farbe",
2384
+ tr.addLanguage("en", {
2385
+ lang_name: "Eglis", // no worries, the example below will overwrite this value
2201
2386
  });
2202
2387
 
2203
-
2204
- // with placeholders:
2388
+ // overwriting previous translation, now with nested objects and placeholders:
2205
2389
 
2206
2390
  tr.addLanguage("en", {
2207
- "welcome_generic": "Welcome!",
2208
- "welcome_name": "Welcome, %1!",
2209
- "welcome_extended": "Welcome, %1!\nYour last login was on %2\nYou have %3 unread messages",
2391
+ // to get this value, you could call `tr.forLang("en", "lang_name")`
2392
+ lang_name: "English",
2393
+ home_page: {
2394
+ welcome: {
2395
+ generic: "Welcome!",
2396
+ // this can be accessed with `tr("home_page.welcome.name", "John")`
2397
+ name: "Welcome, %1!",
2398
+ extended: "Welcome, %1!\nYour last login was on %2\nYou have %3 unread messages",
2399
+ },
2400
+ },
2210
2401
  });
2211
2402
 
2212
-
2213
2403
  // can be used for different locales too:
2214
2404
 
2215
2405
  tr.addLanguage("en-US", {
2216
- "fries": "french fries",
2406
+ fries: "fries",
2407
+ color: "color",
2217
2408
  });
2409
+
2218
2410
  tr.addLanguage("en-GB", {
2219
- "fries": "chips",
2411
+ fries: "chips",
2412
+ color: "colour",
2220
2413
  });
2221
2414
 
2222
-
2223
2415
  // apply default values for different locales to reduce redundancy in shared translation values:
2224
2416
 
2225
2417
  const translation_de = {
2226
- "greeting": "Guten Tag!",
2227
- "foo": "Foo",
2418
+ greeting: "Guten Tag!",
2419
+ foo: "Foo",
2228
2420
  };
2421
+
2229
2422
  tr.addLanguage("de-DE", translation_de);
2423
+
2230
2424
  tr.addLanguage("de-CH", {
2425
+ // overwrite the "greeting" but keep other keys as they are:
2231
2426
  ...translation_de,
2232
- // overwrite the "greeting" but keep other keys as they are
2233
- "greeting": "Grüezi!",
2427
+ greeting: "Grüezi!",
2234
2428
  });
2429
+
2235
2430
  tr.addLanguage("de-AT", {
2431
+ // overwrite "greeting" again but keep other keys as they are:
2236
2432
  ...translation_de,
2237
- // overwrite "greeting" again but keep other keys as they are
2238
- "greeting": "Grüß Gott!",
2433
+ greeting: "Grüß Gott!",
2239
2434
  });
2240
2435
 
2241
-
2242
- // example for custom pluralization:
2436
+ // example for custom pluralization using a predefined suffix:
2243
2437
 
2244
2438
  tr.addLanguage("en", {
2245
2439
  "cart_items_added-0": "No items were added to the cart",
@@ -2247,27 +2441,47 @@ tr.addLanguage("en", {
2247
2441
  "cart_items_added-n": "Added %1 items to the cart",
2248
2442
  });
2249
2443
 
2250
- /** Returns the translation key with a custom pluralization identifier added to it for the given number of items (or size of Array/NodeList or anything else with a `length` property) */
2251
- function pl(key: string, num: number | Array<unknown> | NodeList | { length: number }) {
2252
- if(typeof num !== "number")
2253
- num = num.length;
2444
+ /** A number or any object with a length or size property */
2445
+ type Numberish = number | Array<unknown> | NodeList | { length: number } | { size: number };
2446
+
2447
+ /**
2448
+ * Returns the translated value given the key with a common pluralization identifier appended to it,
2449
+ * given the number of items (or size of Array/NodeList or anything else with a `length` or `size` property).
2450
+ */
2451
+ function trpl(key: string, num: Numberish, ...values: Stringifiable[]): string {
2452
+ if(typeof num !== "number") {
2453
+ if("length" in num)
2454
+ num = num.length;
2455
+ else if("size" in num)
2456
+ num = num.size;
2457
+ }
2254
2458
 
2459
+ let plKey = key;
2255
2460
  if(num === 0)
2256
- return `${key}-0`;
2461
+ plKey = `${key}-0`;
2257
2462
  else if(num === 1)
2258
- return `${key}-1`;
2463
+ plKey = `${key}-1`;
2259
2464
  else
2260
- return `${key}-n`;
2465
+ plKey = `${key}-n`; // will be the fallback for everything like non-numeric values or NaN
2466
+
2467
+ return tr(plKey, ...values);
2261
2468
  };
2262
2469
 
2470
+ // this has to be called once for tr("key") to work - otherwise you can use tr.forLang("en", "key")
2471
+ tr.setLanguage("en");
2472
+
2263
2473
  const items = [];
2264
- console.log(tr(pl("cart_items_added", items), items.length)); // "No items were added to the cart"
2474
+ console.log(trpl("cart_items_added", items, items.length)); // "No items were added to the cart"
2265
2475
 
2266
2476
  items.push("foo");
2267
- console.log(tr(pl("cart_items_added", items), items.length)); // "Added 1 item to the cart"
2477
+ console.log(trpl("cart_items_added", items, items.length)); // "Added 1 item to the cart"
2268
2478
 
2269
2479
  items.push("bar");
2270
- console.log(tr(pl("cart_items_added", items), items.length)); // "Added 2 items to the cart"
2480
+ console.log(trpl("cart_items_added", items, items.length)); // "Added 2 items to the cart"
2481
+
2482
+ // if you run across cases like this, you need to modify your implementation of `trpl()` accordingly:
2483
+ const someVal = parseInt("not a number");
2484
+ console.log(trpl("cart_items_added", someVal, someVal)); // "Added NaN items to the cart"
2271
2485
  ```
2272
2486
  </details>
2273
2487
 
@@ -2279,10 +2493,11 @@ Usage:
2279
2493
  tr.setLanguage(language: string): void
2280
2494
  ```
2281
2495
 
2282
- Synchronously sets the language that will be used for translations.
2496
+ Synchronously sets the language that will be used for translations by default.
2497
+ Alternatively, you can use [`tr.forLang()`](#trforlang) to get translations in a different language without changing the current language.
2283
2498
  No validation is done on the passed language, so make sure it is correct and it has been added with `tr.addLanguage()` before calling `tr()`
2284
2499
 
2285
- For an example, see [`tr()`](#tr)
2500
+ For an example, please see [`tr()`](#tr)
2286
2501
 
2287
2502
  <br>
2288
2503
 
@@ -2300,7 +2515,7 @@ If no language has been set yet, it will return undefined.
2300
2515
  ### tr.getTranslations()
2301
2516
  Usage:
2302
2517
  ```ts
2303
- tr.getTranslations(language?: string): Record<string, string> | undefined
2518
+ tr.getTranslations(language?: string): Record<string, string | object> | undefined
2304
2519
  ```
2305
2520
 
2306
2521
  Returns the translations of the specified language.
@@ -2313,7 +2528,7 @@ If no translations are found, it will return undefined.
2313
2528
  import { tr } from "@sv443-network/userutils";
2314
2529
 
2315
2530
  tr.addLanguage("en", {
2316
- "welcome": "Welcome",
2531
+ welcome: "Welcome",
2317
2532
  });
2318
2533
 
2319
2534
  console.log(tr.getTranslations()); // undefined
@@ -2469,7 +2684,7 @@ logSomething(barObject); // Type error
2469
2684
 
2470
2685
  <br>
2471
2686
 
2472
- ## NonEmptyArray
2687
+ ### NonEmptyArray
2473
2688
  Usage:
2474
2689
  ```ts
2475
2690
  NonEmptyArray<TItem = unknown>
@@ -2499,7 +2714,7 @@ logFirstItem(["04abc", "69"]); // 4
2499
2714
 
2500
2715
  <br>
2501
2716
 
2502
- ## NonEmptyString
2717
+ ### NonEmptyString
2503
2718
  Usage:
2504
2719
  ```ts
2505
2720
  NonEmptyString<TString extends string>
@@ -2523,7 +2738,7 @@ convertToNumber(""); // type error: Argument of type 'string' is not assign
2523
2738
 
2524
2739
  <br>
2525
2740
 
2526
- ## LooseUnion
2741
+ ### LooseUnion
2527
2742
  Usage:
2528
2743
  ```ts
2529
2744
  LooseUnion<TUnion extends string | number | object>
@@ -2550,7 +2765,7 @@ foo(1); // type error: Argument of type '1' is not assignable to parameter of
2550
2765
 
2551
2766
  <br>
2552
2767
 
2553
- ## Prettify
2768
+ ### Prettify
2554
2769
  Usage:
2555
2770
  ```ts
2556
2771
  Prettify<T>
@@ -2603,6 +2818,29 @@ const bar: Bar = {
2603
2818
  ```
2604
2819
  </details>
2605
2820
 
2821
+ <br><br>
2822
+
2823
+ ### ValueGen
2824
+ Usage:
2825
+ ```ts
2826
+ ValueGen<TValueType>
2827
+ ```
2828
+
2829
+ Describes a value that can be obtained in various ways, including via the type itself, a function that returns the type, a Promise that resolves to the type or either a sync or an async function that returns the type.
2830
+ Use it in the [`consumeGen()`](#consumegen) function to convert the given ValueGen value to the type it represents. Also refer to that function for an example.
2831
+
2832
+ <br><br>
2833
+
2834
+ ### StringGen
2835
+ Usage:
2836
+ ```ts
2837
+ StringGen<TStrUnion>
2838
+ ```
2839
+
2840
+ Describes a string that can be obtained in various ways, including via the type itself, a function that returns the type, a Promise that resolves to the type or either a sync or an async function that returns the type.
2841
+ Contrary to [`ValueGen`](#valuegen), this type allows for specifying a union of strings that the StringGen should yield, as long as it is loosely typed as just `string`.
2842
+ Use it in the [`consumeStringGen()`](#consumestringgen) function to convert the given StringGen value to a plain string. Also refer to that function for an example.
2843
+
2606
2844
  <br><br><br><br>
2607
2845
 
2608
2846
  <!-- #region Footer -->