typed-locales 1.0.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/TODO +2 -0
- package/dist/config.js +9 -0
- package/dist/formatters.js +10 -0
- package/dist/index.js +82 -0
- package/dist/react.js +53 -0
- package/dist/test.js +6 -0
- package/dist/translations/en.js +28 -0
- package/dist/translations/es.js +30 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/validation.js +1 -0
- package/package.json +24 -0
package/TODO
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { initReact } from './react';
|
|
2
|
+
import en from './translations/en';
|
|
3
|
+
const customFormatters = {
|
|
4
|
+
myCustomFormatter: () => 'Hello im custom',
|
|
5
|
+
};
|
|
6
|
+
export const { useTranslation, TranslationProvider } = initReact({
|
|
7
|
+
en,
|
|
8
|
+
es: () => import('./translations/es').then(m => m.default),
|
|
9
|
+
}, 'en', customFormatters);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const formatters = {
|
|
2
|
+
lowercase: (value) => value.toLowerCase(),
|
|
3
|
+
uppercase: (value) => value.toUpperCase(),
|
|
4
|
+
capitalize: (value) => value.charAt(0).toUpperCase() + value.slice(1),
|
|
5
|
+
void: () => '',
|
|
6
|
+
weekday: (value) => new Date(value).toLocaleDateString('en-US', { weekday: 'long' }),
|
|
7
|
+
number: (value) => value.toLocaleString(),
|
|
8
|
+
json: (value) => JSON.stringify(value),
|
|
9
|
+
};
|
|
10
|
+
export default formatters;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import baseFormatters from './formatters';
|
|
2
|
+
// Given a translations object returns a function that can be used to translate keys
|
|
3
|
+
export const getTranslate = (translations, locale, extraFormatters) => {
|
|
4
|
+
const formatters = { ...baseFormatters, ...extraFormatters };
|
|
5
|
+
/**
|
|
6
|
+
* Given a key returns the translated value
|
|
7
|
+
* Supports nested keys, substitutions and plurals
|
|
8
|
+
*/
|
|
9
|
+
function translate(key, ...arguments_) {
|
|
10
|
+
const parts = key.split('.');
|
|
11
|
+
let current = translations;
|
|
12
|
+
for (const part of parts) {
|
|
13
|
+
// @ts-expect-error
|
|
14
|
+
if (current && current[part]) {
|
|
15
|
+
// @ts-expect-error
|
|
16
|
+
current = current[part];
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line sonarjs/different-types-comparison
|
|
23
|
+
if (current === undefined) {
|
|
24
|
+
console.error(`Translation key "${key}" not found`);
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
if (typeof current === 'object') {
|
|
28
|
+
// If its an object being returned check if its a plural key
|
|
29
|
+
const isPlural =
|
|
30
|
+
// @ts-expect-error
|
|
31
|
+
current[`${parts.at(-1)}_none`] !== undefined ||
|
|
32
|
+
// @ts-expect-error
|
|
33
|
+
current[`${parts.at(-1)}_one`] !== undefined ||
|
|
34
|
+
// @ts-expect-error
|
|
35
|
+
current[`${parts.at(-1)}_other`] !== undefined;
|
|
36
|
+
if (isPlural) {
|
|
37
|
+
if (!arguments_[0] || !Object.hasOwn(arguments_[0], 'count')) {
|
|
38
|
+
console.error(`Missing count value for plural key "${key}"`);
|
|
39
|
+
return key;
|
|
40
|
+
}
|
|
41
|
+
// @ts-expect-error
|
|
42
|
+
const count = Number(arguments_[0].count);
|
|
43
|
+
if (!count) {
|
|
44
|
+
// @ts-expect-error
|
|
45
|
+
current = current[`${parts.at(-1)}_none`];
|
|
46
|
+
}
|
|
47
|
+
else if (count === 1) {
|
|
48
|
+
// @ts-expect-error
|
|
49
|
+
current = current[`${parts.at(-1)}_one`];
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// @ts-expect-error
|
|
53
|
+
current = current[`${parts.at(-1)}_other`];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.error(`Incomplete translation key "${key}"`);
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let value = String(current);
|
|
62
|
+
const parameters = arguments_[0];
|
|
63
|
+
if (parameters) {
|
|
64
|
+
for (const [parameter, value_] of Object.entries(parameters)) {
|
|
65
|
+
value = value.replaceAll(new RegExp(`{${parameter}(\\|[a-z|]+)?}`, 'g'), (match, formatters_) => {
|
|
66
|
+
const parsedFormatters = (formatters_?.split('|').filter(Boolean) ?? []);
|
|
67
|
+
let formattedValue = String(value_);
|
|
68
|
+
for (const formatter of parsedFormatters) {
|
|
69
|
+
if (!formatters[formatter]) {
|
|
70
|
+
console.error(`Formatter "${formatter}" not found used in key "${key}"`);
|
|
71
|
+
return match;
|
|
72
|
+
}
|
|
73
|
+
formattedValue = formatters[formatter](formattedValue, locale);
|
|
74
|
+
}
|
|
75
|
+
return formattedValue;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
return translate;
|
|
82
|
+
};
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState } from 'react';
|
|
3
|
+
import { getTranslate } from './index';
|
|
4
|
+
// Initial translation always should be loaded
|
|
5
|
+
export const initReact = (translations, initialLocale, extraFormatters) => {
|
|
6
|
+
const TranslationContext = createContext(undefined);
|
|
7
|
+
const TranslationProvider = ({ children }) => {
|
|
8
|
+
const [locale, setLocale] = useState(initialLocale);
|
|
9
|
+
const [translate, setTranslate] = useState(() => getTranslate(translations[locale], locale, extraFormatters));
|
|
10
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
11
|
+
const loadTranslation = async (targetLocale) => {
|
|
12
|
+
try {
|
|
13
|
+
const translationOrLoader = translations[targetLocale];
|
|
14
|
+
let translationData;
|
|
15
|
+
if (typeof translationOrLoader === 'function') {
|
|
16
|
+
setIsLoading(true);
|
|
17
|
+
translationData = await translationOrLoader();
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
translationData = translationOrLoader;
|
|
21
|
+
}
|
|
22
|
+
setTranslate(getTranslate(translationData, targetLocale, extraFormatters));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error(`Failed to load translations for locale ${String(targetLocale)}:`, error);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
setIsLoading(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return (_jsx(TranslationContext.Provider, { value: {
|
|
32
|
+
isLoading,
|
|
33
|
+
locale,
|
|
34
|
+
setLocale: async (newLocale) => {
|
|
35
|
+
if (newLocale !== locale) {
|
|
36
|
+
setLocale(newLocale);
|
|
37
|
+
await loadTranslation(newLocale);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
t: translate,
|
|
41
|
+
}, children: children }));
|
|
42
|
+
};
|
|
43
|
+
const useTranslation = () => {
|
|
44
|
+
const context = useContext(TranslationContext);
|
|
45
|
+
if (!context)
|
|
46
|
+
throw new Error('useTranslation must be used within a TranslationProvider');
|
|
47
|
+
return context;
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
TranslationProvider,
|
|
51
|
+
useTranslation
|
|
52
|
+
};
|
|
53
|
+
};
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from './config';
|
|
3
|
+
const Test = () => {
|
|
4
|
+
const { t, locale, setLocale } = useTranslation();
|
|
5
|
+
return _jsxs("div", { children: [t('test'), t('nested.test', { translation: 'translated text' }), t('nested.deep.again', { value: 'someValue', otherValue: 'anotherValue' }), t('withValue', { value: 'myValue' }), t('multipleValues', { one: '1', two: '2', three: '3' }), t('examplePlural', { count: 0 }), t('examplePlural', { count: 1 }), t('examplePlural', { count: 5 }), t('examplePluralWithOtherValues', { count: 0, user: 'Alice', otherUser: undefined }), t('examplePluralWithOtherValues', { count: 1, user: 'Alice', otherUser: undefined }), t('examplePluralWithOtherValues', { count: 123, user: 'Alice', otherUser: 'Bob' }), t('exampleWithFormatting', { value: 'TEXT', other: 'Text' }), t('exampleWithJSONFormatter', { data: { key: 'value' } }), t('pluralWithNestedSubstitution', { count: 0, query: 'search', user: undefined }), t('pluralWithNestedSubstitution', { count: 1, query: 'search', user: 'john' }), t('pluralWithNestedSubstitution', { count: 5, query: 'search', user: 'john' }), t('mixedPluralNested', { count: 0, itemType: 'book', location: 'shelf' }), t('mixedPluralNested', { count: 1, itemType: 'book', location: 'shelf' }), t('mixedPluralNested', { count: 10, itemType: 'book', location: 'shelf' }), t('onlyFormat', { value: 'capitalize this' }), t('escapeBraces'), locale, _jsx("button", { onClick: () => setLocale('es'), children: "Change locale" })] });
|
|
6
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const en = {
|
|
2
|
+
test: 'Regular translation',
|
|
3
|
+
nested: {
|
|
4
|
+
test: 'Nested {translation|myCustomFormatter}',
|
|
5
|
+
deep: {
|
|
6
|
+
again: 'Nested again with {value} and {otherValue}',
|
|
7
|
+
},
|
|
8
|
+
},
|
|
9
|
+
withValue: 'With value {value}',
|
|
10
|
+
multipleValues: 'Multiple values: {one}, {two}, and {three}',
|
|
11
|
+
examplePlural_none: 'No items available',
|
|
12
|
+
examplePlural_one: 'One item available',
|
|
13
|
+
examplePlural_other: '{count} items available',
|
|
14
|
+
examplePluralWithOtherValues_none: 'No items for {user}',
|
|
15
|
+
examplePluralWithOtherValues_one: 'One item for {user}',
|
|
16
|
+
examplePluralWithOtherValues_other: '{count} items for {user} and {otherUser}',
|
|
17
|
+
exampleWithFormatting: 'Formatted {value|uppercase} text and {other|lowercase}',
|
|
18
|
+
exampleWithJSONFormatter: 'JSON formatter: {data|json}',
|
|
19
|
+
pluralWithNestedSubstitution_none: 'No results found for {query}',
|
|
20
|
+
pluralWithNestedSubstitution_one: 'One result for {query} with {user|capitalize}',
|
|
21
|
+
pluralWithNestedSubstitution_other: '{count} results for {query} by {user|capitalize}',
|
|
22
|
+
mixedPluralNested_none: 'No {itemType} in {location}',
|
|
23
|
+
mixedPluralNested_one: 'One {itemType} in {location|uppercase}',
|
|
24
|
+
mixedPluralNested_other: '{count} {itemType}s in {location|uppercase}',
|
|
25
|
+
onlyFormat: 'Just formatting: {value|capitalize}',
|
|
26
|
+
escapeBraces: 'Braces like this: \\{notAKey\\}',
|
|
27
|
+
};
|
|
28
|
+
export default en;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const es = {
|
|
2
|
+
test: 'Traducción regular',
|
|
3
|
+
nested: {
|
|
4
|
+
test: 'Anidado {translation|myCustomFormatter}',
|
|
5
|
+
deep: {
|
|
6
|
+
again: 'Anidado nuevamente con {value} y {otherValue}',
|
|
7
|
+
},
|
|
8
|
+
},
|
|
9
|
+
withValue: 'Con valor {value}',
|
|
10
|
+
multipleValues: 'Múltiples valores: {one}, {two} y {three}',
|
|
11
|
+
// @ts-expect-error
|
|
12
|
+
examplePlural_none: 'No hay elementos disponibles',
|
|
13
|
+
// @ts-expect-error
|
|
14
|
+
examplePlural_one: 'Un elemento disponible',
|
|
15
|
+
examplePlural_other: '{count} elementos disponibles',
|
|
16
|
+
examplePluralWithOtherValues_none: 'No hay elementos para {user}',
|
|
17
|
+
examplePluralWithOtherValues_one: 'Un elemento para {user}',
|
|
18
|
+
examplePluralWithOtherValues_other: '{count} elementos para {user} y {otherUser}',
|
|
19
|
+
exampleWithFormatting: 'Texto formateado {value|uppercase} y {other|lowercase}',
|
|
20
|
+
exampleWithJSONFormatter: 'Formateador JSON: {data|json}',
|
|
21
|
+
pluralWithNestedSubstitution_none: 'No se encontraron resultados para {query}',
|
|
22
|
+
pluralWithNestedSubstitution_one: 'Un resultado para {query} con {user|capitalize}',
|
|
23
|
+
pluralWithNestedSubstitution_other: '{count} resultados para {query} por {user|capitalize}',
|
|
24
|
+
mixedPluralNested_none: 'No hay {itemType} en {location}',
|
|
25
|
+
mixedPluralNested_one: 'Un {itemType} en {location|uppercase}',
|
|
26
|
+
mixedPluralNested_other: '{count} {itemType}s en {location|uppercase}',
|
|
27
|
+
onlyFormat: 'Solo formateo: {value|capitalize}',
|
|
28
|
+
escapeBraces: 'Llaves como estas: \\{notAKey\\}',
|
|
29
|
+
};
|
|
30
|
+
export default es;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/formatters.ts","../src/index.ts","../src/react.tsx","../src/test.tsx","../src/translations/en.ts","../src/translations/es.ts"],"errors":true,"version":"5.8.2"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "typed-locales",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Type safe utilities for translating strings",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsx watch src/test.tsx"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"packageManager": "pnpm@10.6.2",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"react": "^19.1.0",
|
|
17
|
+
"react-dom": "^19.1.0",
|
|
18
|
+
"tsx": "^4.19.4",
|
|
19
|
+
"typescript": "^5.8.3"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/react": "^19.1.5"
|
|
23
|
+
}
|
|
24
|
+
}
|