typed-locales 1.0.1 → 1.0.3

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.
@@ -0,0 +1,45 @@
1
+ import baseFormatters from './src/formatters';
2
+ import type { Formatter } from './src/formatters';
3
+ type ValueType = null | number | string | undefined | object;
4
+ export type BaseFormatters = keyof typeof baseFormatters;
5
+ export type InternalDeepStringify<T> = {
6
+ [K in keyof T]: T[K] extends string ? string : T[K] extends object ? InternalDeepStringify<T[K]> : never;
7
+ };
8
+ export type DeepStringify<T> = RemoveReadonlyDeep<InternalDeepStringify<T>>;
9
+ export type InternalRemoveReadonlyDeep<T> = {
10
+ -readonly [K in keyof T]: T[K] extends object ? InternalRemoveReadonlyDeep<T[K]> : T[K];
11
+ };
12
+ export type RemoveReadonlyDeep<T> = Simplify<InternalRemoveReadonlyDeep<T>>;
13
+ type PluralSuffix = '_none' | '_one' | '_other';
14
+ export type TranslationType = {
15
+ [key: string]: string | TranslationType;
16
+ };
17
+ type RemovePluralSuffix<T extends string> = T extends `${infer Base}${PluralSuffix}` ? Base : T;
18
+ type PluralKeys<Base extends string> = `${Base}${PluralSuffix}`;
19
+ type DotNestedLeafKeys<T> = {
20
+ [K in keyof T]: K extends string ? T[K] extends Record<string, any> ? `${K}.${DotNestedLeafKeys<T[K]>}` : RemovePluralSuffix<K> : never;
21
+ }[keyof T];
22
+ type GenerateStringFromProperties<T extends Record<string, any>> = T extends Record<string, never> ? string : `${string}{${keyof T & string}${string}` | `${string}{${keyof T & string}|${string}}${string}`;
23
+ export type GenerateTranslationType<T> = {
24
+ -readonly [K in keyof T]: T[K] extends object ? Simplify<GenerateTranslationType<T[K]>> : GenerateStringFromProperties<InterpolationProperties<RemovePluralSuffix<T[K] & string>, IsPlural<T, RemovePluralSuffix<K & string>>>>;
25
+ };
26
+ type ExtractPlaceholders<T extends string> = T extends `${infer _Start}{${infer Placeholder}}${infer Rest}` ? (Placeholder extends `${infer Name}|${infer _Formatters}` ? Name : Placeholder) | ExtractPlaceholders<Rest> : never;
27
+ type HasPluralKeys<T, Path extends string> = Path extends `${infer K}.${infer Rest}` ? K extends keyof T ? HasPluralKeys<T[K], Rest> : false : PluralKeys<Path> & keyof T extends never ? false : true;
28
+ type IsPlural<T, Path extends string> = HasPluralKeys<T, Path>;
29
+ type GetValue<T, Path extends string> = Path extends `${infer K}.${infer Rest}` ? K extends keyof T ? GetValue<T[K], Rest> : never : Path extends keyof T ? T[Path] : T[PluralKeys<Path> & keyof T];
30
+ type InterpolationProperties<S extends string, IsPlural extends boolean> = IsPlural extends true ? {
31
+ count: number;
32
+ } & {
33
+ [K in Exclude<ExtractPlaceholders<S>, 'count'>]: ValueType;
34
+ } : ExtractPlaceholders<S> extends never ? {} : {
35
+ [K in ExtractPlaceholders<S>]: ValueType;
36
+ };
37
+ type InternalSimplify<T> = {
38
+ [K in keyof T]: InternalSimplify<T[K]>;
39
+ } & {};
40
+ type DeepOmitNever<T> = {
41
+ [K in keyof T as T[K] extends never ? never : T[K] extends Record<string, any> ? DeepOmitNever<T[K]> extends never ? never : K : K]: T[K] extends Record<string, any> ? DeepOmitNever<T[K]> : T[K];
42
+ } extends infer U ? keyof U extends never ? never : U : never;
43
+ export type Simplify<T> = InternalSimplify<DeepOmitNever<T>>;
44
+ export declare const getTranslate: <Translations, ExtraFormattersType extends string = string, ExtraFormatters extends Record<ExtraFormattersType, Formatter> = Record<ExtraFormattersType, Formatter>>(translations: Translations, locale: string, extraFormatters?: ExtraFormatters) => <Key extends DotNestedLeafKeys<Translations>>(key: Key, ...arguments_: InterpolationProperties<GetValue<Translations, Key> & string, IsPlural<Translations, Key>> extends Record<string, never> ? [] : [params: Simplify<InterpolationProperties<GetValue<Translations, Key> & string, IsPlural<Translations, Key>>>]) => GetValue<Translations, Key>;
45
+ export { initReact } from './src/react';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import baseFormatters from './formatters';
1
+ import baseFormatters from './src/formatters';
2
2
  // Given a translations object returns a function that can be used to translate keys
3
3
  export const getTranslate = (translations, locale, extraFormatters) => {
4
4
  const formatters = { ...baseFormatters, ...extraFormatters };
@@ -80,3 +80,4 @@ export const getTranslate = (translations, locale, extraFormatters) => {
80
80
  }
81
81
  return translate;
82
82
  };
83
+ export { initReact } from './src/react';
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "typed-locales",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Type safe utilities for translating strings",
5
- "main": "index.js",
5
+ "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "build": "tsc",
8
+ "build": "rimraf dist && tsc",
9
9
  "dev": "tsx watch src/test.tsx"
10
10
  },
11
11
  "keywords": [],
@@ -15,6 +15,7 @@
15
15
  "dependencies": {
16
16
  "react": "^19.1.0",
17
17
  "react-dom": "^19.1.0",
18
+ "rimraf": "^6.0.1",
18
19
  "tsx": "^4.19.4",
19
20
  "typescript": "^5.8.3"
20
21
  },
package/dist/config.js DELETED
@@ -1,9 +0,0 @@
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);
@@ -1,10 +0,0 @@
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/react.js DELETED
@@ -1,53 +0,0 @@
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 DELETED
@@ -1,6 +0,0 @@
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
- };
@@ -1,28 +0,0 @@
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;
@@ -1,30 +0,0 @@
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;
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
1
- export {};