@pocket-trash/localizations 0.0.3 → 0.0.8
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/LICENSE.md +21 -0
- package/README.md +230 -4
- package/dist/index.d.ts +56 -25
- package/dist/index.js +74 -44
- package/dist/localizations/en-US.d.ts +24 -0
- package/dist/localizations/en-US.js +24 -0
- package/dist/localizations/es-MX.d.ts +25 -0
- package/dist/localizations/es-MX.js +25 -0
- package/dist/types/localization.d.ts +25 -0
- package/dist/types/localization.js +1 -0
- package/package.json +19 -10
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pocket Trash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# @pocket-trash/localizations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@pocket-trash/localizations)
|
|
4
|
+
[](./package.json)
|
|
5
|
+
[](https://github.com/Pocket-Trash/pocket-trash-localizations/actions/workflows/ci.yml)
|
|
6
|
+
[](docs/contributing.md)
|
|
7
|
+
|
|
8
|
+
Shared localization utilities and translation catalogs for Pocket Trash.
|
|
9
|
+
|
|
10
|
+
## Documentation
|
|
11
|
+
|
|
12
|
+
| File | Description |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| [contributing.md](docs/contributing.md) | Explains how to edit translations, run checks, commit changes, and open a pull request. |
|
|
15
|
+
| [publishing.md](docs/publishing.md) | Explains how changesets become package versions and npm releases. |
|
|
4
16
|
|
|
5
17
|
## Install
|
|
6
18
|
|
|
@@ -8,20 +20,234 @@ Shared localization utilities and message catalogs for Pocket Trash.
|
|
|
8
20
|
npm install @pocket-trash/localizations
|
|
9
21
|
```
|
|
10
22
|
|
|
11
|
-
##
|
|
23
|
+
## Quick Start
|
|
12
24
|
|
|
13
25
|
```ts
|
|
14
|
-
import {
|
|
26
|
+
import { formatTranslation, resolveLocale } from '@pocket-trash/localizations';
|
|
15
27
|
|
|
16
28
|
const locale = resolveLocale('es-MX');
|
|
17
|
-
const label =
|
|
29
|
+
const label = formatTranslation('action.save', {}, locale);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`label` is `"Guardar"`.
|
|
33
|
+
|
|
34
|
+
## Formatting Translations
|
|
35
|
+
|
|
36
|
+
Use `formatTranslation(key, values, locale)` when you need one translated
|
|
37
|
+
string.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { formatTranslation } from '@pocket-trash/localizations';
|
|
41
|
+
|
|
42
|
+
formatTranslation('action.save');
|
|
43
|
+
formatTranslation('locale.current', { locale: 'es-MX' }, 'es-MX');
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The second argument is the placeholder values object. Pass `{}` when the
|
|
47
|
+
translation does not need any values.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
formatTranslation('action.save', {}, 'es-MX');
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For translations with placeholders, pass the placeholder names and values:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
formatTranslation('locale.current', { locale: 'es-MX' }, 'es-MX');
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
That fills `{locale}` in this translation:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export const esMX = {
|
|
63
|
+
locale: {
|
|
64
|
+
current: 'Idioma actual: {locale}',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If a placeholder value is missing, the placeholder stays in the string. That
|
|
70
|
+
makes missing data visible instead of silently deleting text.
|
|
71
|
+
|
|
72
|
+
## Resolving Locales
|
|
73
|
+
|
|
74
|
+
Use `resolveLocale()` before formatting when locale input comes from a browser,
|
|
75
|
+
request header, user setting, or other string source.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { resolveLocale } from '@pocket-trash/localizations';
|
|
79
|
+
|
|
80
|
+
const locale = resolveLocale('fr-CA', 'es-MX');
|
|
81
|
+
// locale is "es-MX"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`resolveLocale()` returns a TypeScript-safe `SupportedLocale`.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import type { SupportedLocale } from '@pocket-trash/localizations';
|
|
88
|
+
|
|
89
|
+
const locale: SupportedLocale = resolveLocale('nl-BE');
|
|
90
|
+
// locale is "en-US" because nl-BE is not supported
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Passing an unsupported locale to `resolveLocale()` does not throw. It falls
|
|
94
|
+
back to `en-US`.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const locale = resolveLocale('nl-BE');
|
|
98
|
+
formatTranslation('action.save', {}, locale);
|
|
99
|
+
// "Save"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The `locale` parameter on `formatTranslation()` is TypeScript-safe. Passing an
|
|
103
|
+
unsupported literal like `'nl-BE'` directly is a type error.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
formatTranslation('action.save', {}, 'nl-BE');
|
|
107
|
+
// TypeScript error: "nl-BE" is not assignable to SupportedLocale
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Use `resolveLocale()` for raw external input. It returns a supported locale that
|
|
111
|
+
can be passed to `formatTranslation()`.
|
|
112
|
+
|
|
113
|
+
## TypeScript Safety
|
|
114
|
+
|
|
115
|
+
Translation keys are type-safe when you use a known key.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import type { TranslationKey } from '@pocket-trash/localizations';
|
|
119
|
+
|
|
120
|
+
const key: TranslationKey = 'action.save';
|
|
18
121
|
```
|
|
19
122
|
|
|
123
|
+
The current translation keys are:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
type TranslationKey =
|
|
127
|
+
| 'app.name'
|
|
128
|
+
| 'action.cancel'
|
|
129
|
+
| 'action.save'
|
|
130
|
+
| 'error.generic'
|
|
131
|
+
| 'locale.current';
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`formatTranslation()` only accepts known `TranslationKey` values. Unknown keys
|
|
135
|
+
are TypeScript errors.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
formatTranslation('unknown.key');
|
|
139
|
+
// TypeScript error: "unknown.key" is not assignable to TranslationKey
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Getting a Whole Catalog
|
|
143
|
+
|
|
144
|
+
Use `getTranslations()` when you need all translations for a locale.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { getTranslations } from '@pocket-trash/localizations';
|
|
148
|
+
|
|
149
|
+
const translations = getTranslations('es-MX');
|
|
150
|
+
const save = translations['action.save'];
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Returned catalogs are complete. If a non-English locale omits a translation,
|
|
154
|
+
or has a blank string, that key falls back to the `en-US` value.
|
|
155
|
+
|
|
156
|
+
## Clerk Localizations
|
|
157
|
+
|
|
158
|
+
Use `getClerkLocalization()` when configuring Clerk UI components.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { getClerkLocalization, resolveLocale } from '@pocket-trash/localizations';
|
|
162
|
+
|
|
163
|
+
const locale = resolveLocale('es-MX');
|
|
164
|
+
const localization = getClerkLocalization(locale);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
English returns `undefined`, so Clerk uses its default English strings. Spanish
|
|
168
|
+
returns Clerk's `esMX` localization.
|
|
169
|
+
|
|
170
|
+
## Adding Translations
|
|
171
|
+
|
|
172
|
+
Source locale files use nested objects:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
export const enUS = {
|
|
176
|
+
app: {
|
|
177
|
+
name: 'Pocket Trash',
|
|
178
|
+
},
|
|
179
|
+
action: {
|
|
180
|
+
cancel: 'Cancel',
|
|
181
|
+
save: 'Save',
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Callers still use dot keys:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
formatTranslation('action.save');
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Add new translation keys in two places:
|
|
193
|
+
|
|
194
|
+
1. Add the nested field to `CompleteLocalizationResource` in `src/types/localization.ts`.
|
|
195
|
+
2. Add the required English value to `src/localizations/en-US.ts`.
|
|
196
|
+
3. Run `pnpm localization:sync`.
|
|
197
|
+
4. Fill in translated values when available.
|
|
198
|
+
5. Run `pnpm test`.
|
|
199
|
+
|
|
200
|
+
Non-English locale files can omit new keys until translations are ready. Missing
|
|
201
|
+
or blank keys fall back to `en-US` at runtime.
|
|
202
|
+
|
|
203
|
+
## Scaffolding a Locale
|
|
204
|
+
|
|
205
|
+
Create and wire a new locale:
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
pnpm localization:create nl-BE
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
That command:
|
|
212
|
+
|
|
213
|
+
1. Creates `src/localizations/nl-BE.ts`.
|
|
214
|
+
2. Copies the nested `en-US` shape with every value set to `''`.
|
|
215
|
+
3. Adds the locale to `SupportedLocale`, `SUPPORTED_LOCALES`, `localizations`,
|
|
216
|
+
and `translations`.
|
|
217
|
+
4. Adds a Clerk localization mapping if `@clerk/localizations` exports one.
|
|
218
|
+
|
|
219
|
+
Sync missing keys into existing non-English locales:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
pnpm localization:sync
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`en-US` is the source of truth. Sync adds missing nested keys with blank values
|
|
226
|
+
and preserves existing translations.
|
|
227
|
+
|
|
20
228
|
## Develop
|
|
229
|
+
Common commands:
|
|
230
|
+
|
|
231
|
+
| Command | Description |
|
|
232
|
+
| --- | --- |
|
|
233
|
+
| `pnpm install` | Install dependencies. |
|
|
234
|
+
| `pnpm build` | Build the package with TypeScript. |
|
|
235
|
+
| `pnpm format` | Format files with Biome. |
|
|
236
|
+
| `pnpm lint` | Check formatting and lint rules with Biome. |
|
|
237
|
+
| `pnpm check` | Run Biome checks and apply safe fixes. |
|
|
238
|
+
| `pnpm test` | Build the package and run the test suite. |
|
|
239
|
+
| `pnpm localization:create nl-BE` | Create and wire a new locale from the `en-US` source shape. |
|
|
240
|
+
| `pnpm localization:sync` | Add missing source keys to existing non-English locale files. |
|
|
241
|
+
| `pnpm changeset` | Create a release changeset for a pull request. |
|
|
242
|
+
| `pnpm release` | Publish pending package changes from the release workflow. |
|
|
21
243
|
|
|
22
244
|
```sh
|
|
23
245
|
pnpm install
|
|
246
|
+
pnpm format
|
|
247
|
+
pnpm lint
|
|
24
248
|
pnpm test
|
|
249
|
+
pnpm localization:create nl-BE
|
|
250
|
+
pnpm localization:sync
|
|
25
251
|
```
|
|
26
252
|
|
|
27
253
|
Publishing notes live in [docs/publishing.md](docs/publishing.md).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,31 +1,62 @@
|
|
|
1
|
-
import { esMX } from
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
export
|
|
1
|
+
import { esMX as clerkEsMX } from "@clerk/localizations";
|
|
2
|
+
import type { TranslationKey, Translations } from "./types/localization.js";
|
|
3
|
+
export { enUS } from "./localizations/en-US.js";
|
|
4
|
+
export { esMX } from "./localizations/es-MX.js";
|
|
5
|
+
export type { CompleteLocalizationResource, LocalizationResource, TranslationKey, Translations, } from "./types/localization.js";
|
|
6
|
+
export type SupportedLocale = "en-US" | "es-MX";
|
|
7
|
+
export type MessageKey = TranslationKey;
|
|
8
|
+
export type Messages = Translations;
|
|
5
9
|
export type LocalePreference = string | readonly string[] | null | undefined;
|
|
6
|
-
export type ClerkLocalizationResource = typeof
|
|
7
|
-
export declare const DEFAULT_LOCALE = "en";
|
|
8
|
-
export declare const SUPPORTED_LOCALES: readonly ["en", "es-MX"];
|
|
10
|
+
export type ClerkLocalizationResource = typeof clerkEsMX;
|
|
11
|
+
export declare const DEFAULT_LOCALE = "en-US";
|
|
12
|
+
export declare const SUPPORTED_LOCALES: readonly ["en-US", "es-MX"];
|
|
13
|
+
export declare const localizations: Readonly<{
|
|
14
|
+
"en-US": {
|
|
15
|
+
app: {
|
|
16
|
+
name: string;
|
|
17
|
+
};
|
|
18
|
+
action: {
|
|
19
|
+
cancel: string;
|
|
20
|
+
save: string;
|
|
21
|
+
};
|
|
22
|
+
error: {
|
|
23
|
+
generic: string;
|
|
24
|
+
};
|
|
25
|
+
locale: {
|
|
26
|
+
current: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
"es-MX": {
|
|
30
|
+
app: {
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
action: {
|
|
34
|
+
cancel: string;
|
|
35
|
+
save: string;
|
|
36
|
+
};
|
|
37
|
+
error: {
|
|
38
|
+
generic: string;
|
|
39
|
+
};
|
|
40
|
+
locale: {
|
|
41
|
+
current: string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}>;
|
|
45
|
+
export declare const translationKeys: readonly TranslationKey[];
|
|
46
|
+
export declare const translations: Readonly<{
|
|
47
|
+
"en-US": Readonly<Readonly<Record<TranslationKey, string>>>;
|
|
48
|
+
"es-MX": Readonly<Readonly<Record<TranslationKey, string>>>;
|
|
49
|
+
}>;
|
|
9
50
|
export declare const messages: Readonly<{
|
|
10
|
-
en: Readonly<
|
|
11
|
-
|
|
12
|
-
'action.cancel': "Cancel";
|
|
13
|
-
'action.save': "Save";
|
|
14
|
-
'error.generic': "Something went wrong.";
|
|
15
|
-
'locale.current': "Current language: {locale}";
|
|
16
|
-
}>;
|
|
17
|
-
'es-MX': Readonly<{
|
|
18
|
-
'app.name': "Pocket Trash";
|
|
19
|
-
'action.cancel': "Cancelar";
|
|
20
|
-
'action.save': "Guardar";
|
|
21
|
-
'error.generic': "Algo salio mal.";
|
|
22
|
-
'locale.current': "Idioma actual: {locale}";
|
|
23
|
-
}>;
|
|
51
|
+
"en-US": Readonly<Readonly<Record<TranslationKey, string>>>;
|
|
52
|
+
"es-MX": Readonly<Readonly<Record<TranslationKey, string>>>;
|
|
24
53
|
}>;
|
|
25
|
-
export declare const messageKeys: readonly
|
|
54
|
+
export declare const messageKeys: readonly TranslationKey[];
|
|
26
55
|
export declare const clerkLocalizations: Readonly<Record<SupportedLocale, ClerkLocalizationResource | undefined>>;
|
|
27
56
|
export declare function resolveLocale(...preferences: readonly LocalePreference[]): SupportedLocale;
|
|
28
|
-
export declare function
|
|
29
|
-
export declare
|
|
30
|
-
export declare function
|
|
57
|
+
export declare function getTranslations(locale?: SupportedLocale | null): Translations;
|
|
58
|
+
export declare const getMessages: typeof getTranslations;
|
|
59
|
+
export declare function getClerkLocalization(locale?: SupportedLocale | null): ClerkLocalizationResource | undefined;
|
|
60
|
+
export declare function formatTranslation(key: TranslationKey, values?: Readonly<Record<string, unknown>>, locale?: SupportedLocale | null): string;
|
|
61
|
+
export declare const formatMessage: typeof formatTranslation;
|
|
31
62
|
export declare function assertCompleteCatalogs(): true;
|
package/dist/index.js
CHANGED
|
@@ -1,34 +1,31 @@
|
|
|
1
|
-
import { esMX } from
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { esMX as clerkEsMX } from "@clerk/localizations";
|
|
2
|
+
import { enUS } from "./localizations/en-US.js";
|
|
3
|
+
import { esMX } from "./localizations/es-MX.js";
|
|
4
|
+
export { enUS } from "./localizations/en-US.js";
|
|
5
|
+
export { esMX } from "./localizations/es-MX.js";
|
|
6
|
+
export const DEFAULT_LOCALE = "en-US";
|
|
7
|
+
export const SUPPORTED_LOCALES = Object.freeze(["en-US", "es-MX"]);
|
|
4
8
|
const localeAliases = new Map([
|
|
5
|
-
[
|
|
6
|
-
[
|
|
7
|
-
[
|
|
8
|
-
[
|
|
9
|
-
['es-mx', 'es-MX'],
|
|
10
|
-
['es-us', 'es-MX'],
|
|
9
|
+
["es", "es-MX"],
|
|
10
|
+
["es-mx", "es-MX"],
|
|
11
|
+
["es-us", "es-MX"],
|
|
12
|
+
["en-us", "en-US"],
|
|
11
13
|
]);
|
|
12
|
-
export const
|
|
13
|
-
en:
|
|
14
|
-
|
|
15
|
-
'action.cancel': 'Cancel',
|
|
16
|
-
'action.save': 'Save',
|
|
17
|
-
'error.generic': 'Something went wrong.',
|
|
18
|
-
'locale.current': 'Current language: {locale}',
|
|
19
|
-
}),
|
|
20
|
-
'es-MX': Object.freeze({
|
|
21
|
-
'app.name': 'Pocket Trash',
|
|
22
|
-
'action.cancel': 'Cancelar',
|
|
23
|
-
'action.save': 'Guardar',
|
|
24
|
-
'error.generic': 'Algo salio mal.',
|
|
25
|
-
'locale.current': 'Idioma actual: {locale}',
|
|
26
|
-
}),
|
|
14
|
+
export const localizations = Object.freeze({
|
|
15
|
+
"en-US": enUS,
|
|
16
|
+
"es-MX": esMX,
|
|
27
17
|
});
|
|
28
|
-
|
|
18
|
+
const defaultTranslations = flattenLocalization(enUS);
|
|
19
|
+
export const translationKeys = Object.freeze(Object.keys(defaultTranslations));
|
|
20
|
+
export const translations = Object.freeze({
|
|
21
|
+
"en-US": Object.freeze(defaultTranslations),
|
|
22
|
+
"es-MX": Object.freeze(mergeTranslations(defaultTranslations, flattenLocalization(esMX))),
|
|
23
|
+
});
|
|
24
|
+
export const messages = translations;
|
|
25
|
+
export const messageKeys = translationKeys;
|
|
29
26
|
export const clerkLocalizations = Object.freeze({
|
|
30
|
-
en: undefined,
|
|
31
|
-
|
|
27
|
+
"en-US": undefined,
|
|
28
|
+
"es-MX": clerkEsMX,
|
|
32
29
|
});
|
|
33
30
|
export function resolveLocale(...preferences) {
|
|
34
31
|
for (const preference of preferences.flat()) {
|
|
@@ -40,39 +37,47 @@ export function resolveLocale(...preferences) {
|
|
|
40
37
|
}
|
|
41
38
|
return DEFAULT_LOCALE;
|
|
42
39
|
}
|
|
43
|
-
export function
|
|
44
|
-
return
|
|
40
|
+
export function getTranslations(locale) {
|
|
41
|
+
return translations[resolveLocale(locale)];
|
|
45
42
|
}
|
|
43
|
+
export const getMessages = getTranslations;
|
|
46
44
|
export function getClerkLocalization(locale) {
|
|
47
45
|
return clerkLocalizations[resolveLocale(locale)];
|
|
48
46
|
}
|
|
49
|
-
export function
|
|
50
|
-
const template =
|
|
47
|
+
export function formatTranslation(key, values = {}, locale = DEFAULT_LOCALE) {
|
|
48
|
+
const template = getTranslations(locale)[key] ?? translations[DEFAULT_LOCALE][key];
|
|
51
49
|
if (!template)
|
|
52
50
|
return key;
|
|
53
51
|
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => values[name] == null ? `{${name}}` : String(values[name]));
|
|
54
52
|
}
|
|
53
|
+
export const formatMessage = formatTranslation;
|
|
55
54
|
export function assertCompleteCatalogs() {
|
|
56
|
-
const expected = new Set(
|
|
57
|
-
for (const [locale, catalog] of Object.entries(
|
|
58
|
-
const
|
|
59
|
-
const
|
|
55
|
+
const expected = new Set(translationKeys);
|
|
56
|
+
for (const [locale, catalog] of Object.entries(localizations)) {
|
|
57
|
+
const flatCatalog = flattenLocalization(catalog);
|
|
58
|
+
const keys = Object.keys(flatCatalog);
|
|
59
|
+
const missing = locale === DEFAULT_LOCALE
|
|
60
|
+
? translationKeys.filter((key) => !(key in flatCatalog))
|
|
61
|
+
: [];
|
|
62
|
+
const empty = locale === DEFAULT_LOCALE
|
|
63
|
+
? translationKeys.filter((key) => flatCatalog[key] === "")
|
|
64
|
+
: [];
|
|
60
65
|
const extra = keys.filter((key) => !expected.has(key));
|
|
61
|
-
if (missing.length || extra.length) {
|
|
62
|
-
throw new Error(`${locale} catalog mismatch: missing ${missing.join(
|
|
66
|
+
if (missing.length || empty.length || extra.length) {
|
|
67
|
+
throw new Error(`${locale} catalog mismatch: missing ${missing.join(", ") || "none"}; empty ${empty.join(", ") || "none"}; extra ${extra.join(", ") || "none"}`);
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
return true;
|
|
66
71
|
}
|
|
67
72
|
function parseLocalePreference(preference) {
|
|
68
|
-
if (typeof preference !==
|
|
73
|
+
if (typeof preference !== "string" || !preference.trim())
|
|
69
74
|
return [];
|
|
70
|
-
if (!preference.includes(
|
|
75
|
+
if (!preference.includes(",") && !preference.includes(";"))
|
|
71
76
|
return [preference];
|
|
72
77
|
return preference
|
|
73
|
-
.split(
|
|
78
|
+
.split(",")
|
|
74
79
|
.map((part, index) => {
|
|
75
|
-
const [tag, ...params] = part.trim().split(
|
|
80
|
+
const [tag, ...params] = part.trim().split(";");
|
|
76
81
|
const q = params
|
|
77
82
|
.map((param) => param.trim().match(/^q=([0-9.]+)$/i)?.[1])
|
|
78
83
|
.find(Boolean);
|
|
@@ -83,9 +88,34 @@ function parseLocalePreference(preference) {
|
|
|
83
88
|
.map(({ tag }) => tag);
|
|
84
89
|
}
|
|
85
90
|
function matchLocale(tag) {
|
|
86
|
-
const normalized = tag.trim().replace(
|
|
87
|
-
if (normalized ===
|
|
91
|
+
const normalized = tag.trim().replace("_", "-").toLowerCase();
|
|
92
|
+
if (normalized === "*")
|
|
88
93
|
return DEFAULT_LOCALE;
|
|
89
|
-
return localeAliases.get(normalized) ??
|
|
94
|
+
return localeAliases.get(normalized) ?? null;
|
|
95
|
+
}
|
|
96
|
+
function flattenLocalization(resource) {
|
|
97
|
+
const entries = {};
|
|
98
|
+
for (const [key, value] of flattenEntries(resource)) {
|
|
99
|
+
entries[key] = value;
|
|
100
|
+
}
|
|
101
|
+
return entries;
|
|
102
|
+
}
|
|
103
|
+
function flattenEntries(resource, prefix = "") {
|
|
104
|
+
return Object.entries(resource).flatMap(([key, value]) => {
|
|
105
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
106
|
+
if (typeof value === "string")
|
|
107
|
+
return [[path, value]];
|
|
108
|
+
if (value && typeof value === "object")
|
|
109
|
+
return flattenEntries(value, path);
|
|
110
|
+
return [];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function mergeTranslations(defaults, overrides) {
|
|
114
|
+
return Object.fromEntries(translationKeys.map((key) => [
|
|
115
|
+
key,
|
|
116
|
+
overrides[key] === "" || overrides[key] == null
|
|
117
|
+
? defaults[key]
|
|
118
|
+
: overrides[key],
|
|
119
|
+
]));
|
|
90
120
|
}
|
|
91
121
|
assertCompleteCatalogs();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* English (United States) translations for Pocket Trash.
|
|
3
|
+
*
|
|
4
|
+
* This catalog is checked against `CompleteLocalizationResource` from
|
|
5
|
+
* `src/types/localization.ts`. When adding a translation key, update that type
|
|
6
|
+
* first, then add the required English string here. Other locale catalogs can
|
|
7
|
+
* omit most new keys until a translation is available; omitted keys fall back to
|
|
8
|
+
* this `en-US` catalog at runtime.
|
|
9
|
+
*/
|
|
10
|
+
export declare const enUS: {
|
|
11
|
+
app: {
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
action: {
|
|
15
|
+
cancel: string;
|
|
16
|
+
save: string;
|
|
17
|
+
};
|
|
18
|
+
error: {
|
|
19
|
+
generic: string;
|
|
20
|
+
};
|
|
21
|
+
locale: {
|
|
22
|
+
current: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* English (United States) translations for Pocket Trash.
|
|
3
|
+
*
|
|
4
|
+
* This catalog is checked against `CompleteLocalizationResource` from
|
|
5
|
+
* `src/types/localization.ts`. When adding a translation key, update that type
|
|
6
|
+
* first, then add the required English string here. Other locale catalogs can
|
|
7
|
+
* omit most new keys until a translation is available; omitted keys fall back to
|
|
8
|
+
* this `en-US` catalog at runtime.
|
|
9
|
+
*/
|
|
10
|
+
export const enUS = {
|
|
11
|
+
app: {
|
|
12
|
+
name: "Pocket Trash",
|
|
13
|
+
},
|
|
14
|
+
action: {
|
|
15
|
+
cancel: "Cancel",
|
|
16
|
+
save: "Save",
|
|
17
|
+
},
|
|
18
|
+
error: {
|
|
19
|
+
generic: "Something went wrong.",
|
|
20
|
+
},
|
|
21
|
+
locale: {
|
|
22
|
+
current: "Current language: {locale}",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spanish (Mexico) translations for Pocket Trash.
|
|
3
|
+
*
|
|
4
|
+
* This catalog is checked against `LocalizationResource` from
|
|
5
|
+
* `src/types/localization.ts`. When adding a translation key, update that type
|
|
6
|
+
* first, add the required English string in `src/localizations/en-US.ts`, then
|
|
7
|
+
* add this locale's translation when available. Most new keys should remain
|
|
8
|
+
* optional in the type so this catalog can omit untranslated strings and fall
|
|
9
|
+
* back to `en-US` at runtime.
|
|
10
|
+
*/
|
|
11
|
+
export declare const esMX: {
|
|
12
|
+
app: {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
action: {
|
|
16
|
+
cancel: string;
|
|
17
|
+
save: string;
|
|
18
|
+
};
|
|
19
|
+
error: {
|
|
20
|
+
generic: string;
|
|
21
|
+
};
|
|
22
|
+
locale: {
|
|
23
|
+
current: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spanish (Mexico) translations for Pocket Trash.
|
|
3
|
+
*
|
|
4
|
+
* This catalog is checked against `LocalizationResource` from
|
|
5
|
+
* `src/types/localization.ts`. When adding a translation key, update that type
|
|
6
|
+
* first, add the required English string in `src/localizations/en-US.ts`, then
|
|
7
|
+
* add this locale's translation when available. Most new keys should remain
|
|
8
|
+
* optional in the type so this catalog can omit untranslated strings and fall
|
|
9
|
+
* back to `en-US` at runtime.
|
|
10
|
+
*/
|
|
11
|
+
export const esMX = {
|
|
12
|
+
app: {
|
|
13
|
+
name: "Pocket Trash",
|
|
14
|
+
},
|
|
15
|
+
action: {
|
|
16
|
+
cancel: "Cancelar",
|
|
17
|
+
save: "Guardar",
|
|
18
|
+
},
|
|
19
|
+
error: {
|
|
20
|
+
generic: "Algo salio mal.",
|
|
21
|
+
},
|
|
22
|
+
locale: {
|
|
23
|
+
current: "Idioma actual: {locale}",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type DotPaths<T> = {
|
|
2
|
+
[K in keyof T & string]: T[K] extends string ? K : T[K] extends object ? `${K}.${DotPaths<T[K]>}` : never;
|
|
3
|
+
}[keyof T & string];
|
|
4
|
+
type DeepPartial<T> = {
|
|
5
|
+
readonly [K in keyof T]?: T[K] extends string ? string : DeepPartial<T[K]>;
|
|
6
|
+
};
|
|
7
|
+
export type CompleteLocalizationResource = Readonly<{
|
|
8
|
+
app: {
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
action: {
|
|
12
|
+
cancel: string;
|
|
13
|
+
save: string;
|
|
14
|
+
};
|
|
15
|
+
error: {
|
|
16
|
+
generic: string;
|
|
17
|
+
};
|
|
18
|
+
locale: {
|
|
19
|
+
current: string;
|
|
20
|
+
};
|
|
21
|
+
}>;
|
|
22
|
+
export type TranslationKey = DotPaths<CompleteLocalizationResource>;
|
|
23
|
+
export type Translations = Readonly<Record<TranslationKey, string>>;
|
|
24
|
+
export type LocalizationResource = DeepPartial<CompleteLocalizationResource>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pocket-trash/localizations",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Shared localization utilities and message catalogs for Pocket Trash.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,23 +31,32 @@
|
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "tsc -p tsconfig.json",
|
|
36
|
-
"changeset": "changeset",
|
|
37
|
-
"changeset:version": "changeset version",
|
|
38
|
-
"release": "changeset publish",
|
|
39
|
-
"test": "pnpm run build && node --test"
|
|
40
|
-
},
|
|
41
34
|
"engines": {
|
|
42
35
|
"node": ">=20"
|
|
43
36
|
},
|
|
44
37
|
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^2.5.8",
|
|
45
39
|
"@changesets/cli": "^2.31.1",
|
|
40
|
+
"@commitlint/cli": "^21.2.2",
|
|
41
|
+
"@commitlint/config-conventional": "^21.2.2",
|
|
42
|
+
"husky": "^9.1.7",
|
|
46
43
|
"typescript": "^5.9.2"
|
|
47
44
|
},
|
|
48
45
|
"dependencies": {
|
|
49
46
|
"@clerk/localizations": "^4.14.2",
|
|
50
47
|
"lts": "^2.0.0"
|
|
51
48
|
},
|
|
52
|
-
"
|
|
53
|
-
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.json",
|
|
51
|
+
"check": "biome check --write .",
|
|
52
|
+
"changeset": "changeset",
|
|
53
|
+
"changeset:version": "changeset version && node scripts/stamp-changelog-release-date.mjs",
|
|
54
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
55
|
+
"format": "biome format --write .",
|
|
56
|
+
"lint": "biome ci .",
|
|
57
|
+
"localization:create": "node scripts/localizations.mjs create",
|
|
58
|
+
"localization:sync": "node scripts/localizations.mjs sync",
|
|
59
|
+
"release": "pnpm run changeset:version && pnpm install --lockfile-only && changeset publish",
|
|
60
|
+
"test": "pnpm run build && node --test"
|
|
61
|
+
}
|
|
62
|
+
}
|