diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c50c1675 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "coding-conventions"] + path = coding-conventions + url = https://github.com/helpwave/coding-conventions.git diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..c2fc5817 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +public-hoist-pattern[]=*storybook* +public-hoist-pattern[]=*@storybook* diff --git a/apps/hightide-native-storybook/src/Internationalization/ExtendTranslations.stories.tsx b/apps/hightide-native-storybook/src/Internationalization/ExtendTranslations.stories.tsx index 221ae07b..2197bfc1 100644 --- a/apps/hightide-native-storybook/src/Internationalization/ExtendTranslations.stories.tsx +++ b/apps/hightide-native-storybook/src/Internationalization/ExtendTranslations.stories.tsx @@ -47,7 +47,7 @@ const frenchTranslations = { const demoSupportedLocales = { ...HightideConfigUtils.defaultSupportedLocales, - 'fr-FR': { localName: 'Français' }, + 'fr-FR': { localeName: 'Français' }, } const LocaleSelect = () => { @@ -60,7 +60,7 @@ const LocaleSelect = () => { const options = useMemo( () => Object.entries(supportedLocales).map(([id, info]) => ({ id, - label: info.localName, + label: info.localeName, })), [supportedLocales] ) diff --git a/apps/hightide-native-storybook/src/Theme/PrimaryColorChange.stories.tsx b/apps/hightide-native-storybook/src/Theme/PrimaryColorChange.stories.tsx index 7ee3a552..3b6f5551 100644 --- a/apps/hightide-native-storybook/src/Theme/PrimaryColorChange.stories.tsx +++ b/apps/hightide-native-storybook/src/Theme/PrimaryColorChange.stories.tsx @@ -51,6 +51,8 @@ const bluePrimaryDarkTheme = createHightideTheme(createDarkThemeTokens({ const bluePrimarySupportedThemes = { ...HightideConfigUtils.defaultSupportedThemes, 'blue-primary': { + name: 'Blue Primary', + isDark: false, nameTranslations: { 'de-DE': 'Blau Primary', 'en-US': 'Blue Primary', @@ -58,6 +60,8 @@ const bluePrimarySupportedThemes = { theme: bluePrimaryTheme, }, 'blue-primary-dark': { + name: 'Blue Primary Dark', + isDark: true, nameTranslations: { 'de-DE': 'Blau Primary Dunkel', 'en-US': 'Blue Primary Dark', diff --git a/apps/hightide-native-storybook/src/Theme/ThemeExtension.stories.tsx b/apps/hightide-native-storybook/src/Theme/ThemeExtension.stories.tsx index f88ef705..5049b494 100644 --- a/apps/hightide-native-storybook/src/Theme/ThemeExtension.stories.tsx +++ b/apps/hightide-native-storybook/src/Theme/ThemeExtension.stories.tsx @@ -46,6 +46,8 @@ const oceanTheme = createHightideTheme(createLightThemeTokens({ const oceanSupportedThemes = { ...HightideConfigUtils.defaultSupportedThemes, ocean: { + name: 'Ocean', + isDark: false, nameTranslations: { 'de-DE': 'Ozean', 'en-US': 'Ocean', diff --git a/apps/hightide-native-storybook/src/Theme/themeStoryHelpers.tsx b/apps/hightide-native-storybook/src/Theme/themeStoryHelpers.tsx index 6828d526..3c2270f9 100644 --- a/apps/hightide-native-storybook/src/Theme/themeStoryHelpers.tsx +++ b/apps/hightide-native-storybook/src/Theme/themeStoryHelpers.tsx @@ -9,6 +9,10 @@ import { ThemedText } from '@helpwave/hightide-native/components' import { useTheme } from '@helpwave/hightide-native/global-contexts' +import { useLocalization } from '@helpwave/hightide-utils/context/localization' +import type { HightideTranslationEntries } from '@helpwave/hightide-utils/i18n' +import { ThemeUtils } from '@helpwave/hightide-utils/context/theme' +import { useHightideTranslation } from '@helpwave/hightide-utils/context/translation' export const ThemeSelect = () => { const { @@ -16,13 +20,19 @@ export const ThemeSelect = () => { setTheme, supportedThemes, } = useTheme() + const { locale } = useLocalization() + const translation = useHightideTranslation() const options = useMemo( () => Object.entries(supportedThemes).map(([id, info]) => ({ id, - label: info.nameTranslations['en-US'] ?? id, + label: ThemeUtils.resolveThemeInformationLabel( + info, + locale, + (key) => translation(key as keyof HightideTranslationEntries) + ), })), - [supportedThemes] + [locale, supportedThemes, translation] ) return ( diff --git a/coding-conventions b/coding-conventions new file mode 160000 index 00000000..12ed12c7 --- /dev/null +++ b/coding-conventions @@ -0,0 +1 @@ +Subproject commit 12ed12c78a8b1d55eb0ae06d176c9a01103b81a5 diff --git a/conventions/IMPORTS.md b/conventions/IMPORTS.md deleted file mode 100644 index 866add61..00000000 --- a/conventions/IMPORTS.md +++ /dev/null @@ -1,91 +0,0 @@ -# Import conventions - -Applies to all packages in this monorepo (`hightide-web`, `hightide-native`, `hightide-design`, `hightide-utils`). - -## Relative package imports - -Imports from **within the same package** must use relative paths (`./` or `../`), never use path alias like `@/`. - -```ts -// Prefer -import { useTheme } from '../../global-contexts/theme/ThemeContext' -import type { ChatTheme } from '../../theme/types/components/chat' - -// Avoid -import { useTheme } from '@/src/global-contexts/theme/ThemeContext' -import type { ChatTheme } from '@/src/theme/types/components/chat' -``` - -Cross-package imports (for example `@helpwave/hightide-design/primitive-tokens` or `@helpwave/hightide-utils/hooks`) keep their published package names. Do not rewrite those to relative paths across package boundaries. - -Design tokens: import primitives from `@helpwave/hightide-design/primitive-tokens`, theme adapters / `StateBasedProperty` from `/theme-tokens`, semantic mappers from `/semantic-tokens`, component tokens from `/component-tokens`, `designSystem` / construction helpers from `/design-system`, and `HexColorUtils` from `/utils`. - -## No index / barrel imports - -Local package imports must target the **concrete module file**, never a folder `index` barrel. - -```ts -// Prefer -import { useTheme } from '../../global-contexts/theme/ThemeContext' -import type { StyleOverwrite } from '../../theme/types/resolver' -import { Button } from '../user-interaction/Button' - -// Avoid -import { useTheme } from '../../global-contexts/theme' -import type { StyleOverwrite } from '../../theme' -import { Button } from '../user-interaction' -``` - -Barrel `index.ts` files may still exist for package **public** entrypoints. Do not import through them from other files inside this package. - -## Bundle imports from the same module - -Imports from the **same module** must be combined into a single import statement. Do not split value and type imports across multiple statements for one path. - -When an import has more than one binding, write each binding on its own line: - -```ts -import { - ScrollView, - View, - type StyleProp, - type ViewProps, - type ViewStyle -} from 'react-native' -``` - -Single-binding imports may stay on one line: - -```ts -import { useMemo } from 'react' -``` - -## Import order by externality - -Group and order imports from most external to most local: - -1. **External dependencies** — `react`, `react-native`, third-party libraries, Node built-ins -2. **Workspace / sibling packages** — `@helpwave/…` (and other monorepo packages) -3. **Internal package imports** — relative `./` / `../` paths - -Separate groups with a blank line. Within a group, keep a stable alphabetical order when practical. - -```ts -import { - useMemo, - type ReactNode -} from 'react' -import { - Text, - View, - type StyleProp, - type ViewStyle -} from 'react-native' -import { Check } from 'lucide-react-native' - -import { hightideTypography } from '@helpwave/hightide-design/primitive-tokens' -import { HexColorUtils } from '@helpwave/hightide-design/utils' - -import { useTheme } from '../../global-contexts/theme/ThemeContext' -import type { ChatTheme } from '../../theme/types/components/chat' -``` diff --git a/conventions/Typescript-Enum.md b/conventions/Typescript-Enum.md deleted file mode 100644 index cfaee37b..00000000 --- a/conventions/Typescript-Enum.md +++ /dev/null @@ -1,34 +0,0 @@ -### TypeScript Enum Pattern - -When defining a finite set of string values in TypeScript, use the following pattern instead of a TypeScript enum. - -Pattern (replace A with the name of the enum) - -```typescript -// 1. Array definition -const aValues = ["value1", "value2"] as const; -// 2. Type definition -export type A = (typeof values)[number]; -// 3. Set definition -const allowedAValues: ReadonlySet = new Set(values); -// 4. Type-Check definition -function isAValue(value: unknown): value is A { - if (typeof value !== "string") return false; - return allowedAValues.has(value); -} - -// 5. Utils definition -export const AUtils = { - array: aValues, - set: allowedAValues, - typeCheck: isAValue, -}; -``` - -1. enum Name in camelCase + "Values" suffix -2. enum Name -3. "allowed" prefix + enum Name + + "Values" suffix -4. "is" prefix + enum Name + "Value" suffix -5. enum Name + "Utils" suffix - -For the actual implementation remove the comments diff --git a/conventions/PUBLISHING.md b/documentation/PUBLISHING.md similarity index 100% rename from conventions/PUBLISHING.md rename to documentation/PUBLISHING.md diff --git a/packages/hightide-native/CHANGELOG.md b/packages/hightide-native/CHANGELOG.md index 46e83366..65250792 100644 --- a/packages/hightide-native/CHANGELOG.md +++ b/packages/hightide-native/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.4] - unpublished + +### Added + +- The hightide-utils `locales` folder is copied into `dist` during the build and exported as `@helpwave/hightide-native/locales/*` + +### Changed + +- Default locales and theme metadata use `HightideSupportedLocaleInformation` and `HightideSupportedThemeInformation` from hightide-utils. Theme labels use `themeNameLight` / `themeNameDark` + ## [0.10.3] - 2026-10-13 ### Added diff --git a/packages/hightide-native/package.json b/packages/hightide-native/package.json index a008bc53..845656de 100644 --- a/packages/hightide-native/package.json +++ b/packages/hightide-native/package.json @@ -13,7 +13,8 @@ "version": "0.10.3", "type": "module", "files": [ - "src" + "src", + "dist" ], "exports": { "./components": "./src/components/index.ts", @@ -22,12 +23,13 @@ "./icons": "./src/icons/index.ts", "./theme": "./src/theme/index.ts", "./types": "./src/types/index.ts", - "./utils": "./src/utils/index.ts" + "./utils": "./src/utils/index.ts", + "./locales/*": "./dist/locales/*" }, "scripts": { "clean": "rm -rf node_modules", "init": "node -e \"process.exit(0)\"", - "build": "tsc --noEmit", + "build": "tsc --noEmit && rm -rf dist/locales && mkdir -p dist && cp -R ../hightide-utils/locales dist/locales", "dev": "node -e \"process.exit(0)\"", "lint": "eslint .", "test": "node -e \"process.exit(0)\"" diff --git a/packages/hightide-native/src/components/user-interaction/Checkbox.tsx b/packages/hightide-native/src/components/user-interaction/Checkbox.tsx index 6581d815..a90c3b1b 100644 --- a/packages/hightide-native/src/components/user-interaction/Checkbox.tsx +++ b/packages/hightide-native/src/components/user-interaction/Checkbox.tsx @@ -8,7 +8,7 @@ import { } from 'react-native' import { useControlledState, - useEventCallbackStabilizer + useStableEvent } from '@helpwave/hightide-utils/hooks' import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens' @@ -74,8 +74,8 @@ export const Checkbox = ({ const interactive = !disabled && !readOnly const [isPressed, setIsPressed] = useState(false) - const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete) - const onValueChangeStable = useEventCallbackStabilizer(onValueChange) + const onEditCompleteStable = useStableEvent(onEditComplete) + const onValueChangeStable = useStableEvent(onValueChange) const onChangeWrapper = useCallback((nextValue: boolean) => { onValueChangeStable(nextValue) diff --git a/packages/hightide-native/src/components/user-interaction/FileInput/FileInputRoot.tsx b/packages/hightide-native/src/components/user-interaction/FileInput/FileInputRoot.tsx index e1a0f52c..a58ef7ff 100644 --- a/packages/hightide-native/src/components/user-interaction/FileInput/FileInputRoot.tsx +++ b/packages/hightide-native/src/components/user-interaction/FileInput/FileInputRoot.tsx @@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react' import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens' import { useControlledState, - useEventCallbackStabilizer + useStableEvent } from '@helpwave/hightide-utils/hooks' import type { @@ -65,10 +65,10 @@ export function FileInputRoot({ defaultValue: resolveFileInputMaxFiles(maxFiles) === 1 ? false : initialIsOpen, }) - const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete) - const onCloseStable = useEventCallbackStabilizer(onClose) - const onIsOpenChangeStable = useEventCallbackStabilizer(onIsOpenChange) - const pickFilesStable = useEventCallbackStabilizer(pickFiles) + const onEditCompleteStable = useStableEvent(onEditComplete) + const onCloseStable = useStableEvent(onClose) + const onIsOpenChangeStable = useStableEvent(onIsOpenChange) + const pickFilesStable = useStableEvent(pickFiles) const commitFiles = useCallback((next: readonly FileInputItem[]) => { setFiles(next) diff --git a/packages/hightide-native/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx b/packages/hightide-native/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx index c11bfae2..1cce2ffa 100644 --- a/packages/hightide-native/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx +++ b/packages/hightide-native/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react' import { useCallback, useMemo, useState } from 'react' import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens' -import { useEventCallbackStabilizer } from '@helpwave/hightide-utils/hooks' +import { useStableEvent } from '@helpwave/hightide-utils/hooks' import { useMultiSelect } from '../../../hooks/useMultiSelect' import type { @@ -96,8 +96,8 @@ export function MultiSelectRoot({ .filter((id): id is string => id !== undefined) }, [compare, idToOptionMap, initialValue]) - const onValueChangeStable = useEventCallbackStabilizer(onValueChange) - const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete) + const onValueChangeStable = useStableEvent(onValueChange) + const onEditCompleteStable = useStableEvent(onEditComplete) const onValueChangeWrapper = useCallback((ids: string[]) => { const values = ids diff --git a/packages/hightide-native/src/components/user-interaction/Select/SelectRoot.tsx b/packages/hightide-native/src/components/user-interaction/Select/SelectRoot.tsx index b8b81d7d..bdac2bcb 100644 --- a/packages/hightide-native/src/components/user-interaction/Select/SelectRoot.tsx +++ b/packages/hightide-native/src/components/user-interaction/Select/SelectRoot.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react' import { useCallback, useMemo, useState } from 'react' import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens' -import { useEventCallbackStabilizer } from '@helpwave/hightide-utils/hooks' +import { useStableEvent } from '@helpwave/hightide-utils/hooks' import { useSelect } from '../../../hooks/useSelect' import type { @@ -92,9 +92,9 @@ export function SelectRoot({ return Object.values(idToOptionMap).find((option) => compare(option.value.value, initialValue))?.value.id ?? null }, [compare, idToOptionMap, initialValue]) - const onValueChangeStable = useEventCallbackStabilizer(onValueChange) - const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete) - const onIsOpenChangeStable = useEventCallbackStabilizer(onIsOpenChange) + const onValueChangeStable = useStableEvent(onValueChange) + const onEditCompleteStable = useStableEvent(onEditComplete) + const onIsOpenChangeStable = useStableEvent(onIsOpenChange) const onValueChangeWrapper = useCallback((id: string) => { const option = idToOptionMap[id] diff --git a/packages/hightide-native/src/components/user-interaction/Switch.tsx b/packages/hightide-native/src/components/user-interaction/Switch.tsx index 86fcd9dd..4cc4a083 100644 --- a/packages/hightide-native/src/components/user-interaction/Switch.tsx +++ b/packages/hightide-native/src/components/user-interaction/Switch.tsx @@ -14,7 +14,7 @@ import { import { useControlledState, - useEventCallbackStabilizer + useStableEvent } from '@helpwave/hightide-utils/hooks' import { useDebugContext } from '../../global-contexts/debug' @@ -80,8 +80,8 @@ export const Switch = ({ const interactive = !disabled && !readOnly const [isPressed, setIsPressed] = useState(false) - const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete) - const onValueChangeStable = useEventCallbackStabilizer(onValueChange) + const onEditCompleteStable = useStableEvent(onEditComplete) + const onValueChangeStable = useStableEvent(onValueChange) const onChangeWrapper = useCallback((nextValue: boolean) => { onValueChangeStable(nextValue) diff --git a/packages/hightide-native/src/global-contexts/hightide-config/HightideConfigUtils.ts b/packages/hightide-native/src/global-contexts/hightide-config/HightideConfigUtils.ts index 59551d69..fe1a5ba5 100644 --- a/packages/hightide-native/src/global-contexts/hightide-config/HightideConfigUtils.ts +++ b/packages/hightide-native/src/global-contexts/hightide-config/HightideConfigUtils.ts @@ -1,117 +1,21 @@ -import type { SupportedThemesConfig } from '@helpwave/hightide-utils/context' +import { + HightideSupportedLocaleInformation, + HightideSupportedThemeInformation, + type SupportedThemesConfig +} from '@helpwave/hightide-utils/context' -import type { SupportedLocalesConfig } from '../localization' import { themes } from '../../theme/themes/hightideThemes' import type { HightideTheme } from '../../theme/types/theme' export const HightideConfigUtils = { - defaultSupportedLocales: { - 'de-DE': { - localName: 'Deutsch', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - 'en-US': { - localName: 'English (US)', - defaultIs24HourFormat: false, - defaultStartingWeekday: 'sunday', - defaultCalendarType: 'Gregorian', - }, - 'pt-BR': { - localName: 'Português (Brasil)', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'sunday', - defaultCalendarType: 'Gregorian', - }, - 'es-ES': { - localName: 'Español', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - 'fr-FR': { - localName: 'Français', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - 'nl-NL': { - localName: 'Nederlands', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - 'it-IT': { - localName: 'Italiano', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - 'pl-PL': { - localName: 'Polski', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - 'tr-TR': { - localName: 'Türkçe', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - 'ro-RO': { - localName: 'Română', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - 'el-GR': { - localName: 'Ελληνικά', - defaultIs24HourFormat: false, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - 'ru-RU': { - localName: 'Русский', - defaultIs24HourFormat: true, - defaultStartingWeekday: 'monday', - defaultCalendarType: 'Gregorian', - }, - } as const satisfies SupportedLocalesConfig, + defaultSupportedLocales: HightideSupportedLocaleInformation, defaultSupportedThemes: { light: { - nameTranslations: { - 'de-DE': 'Hell', - 'en-US': 'Light', - 'pt-BR': 'Claro', - 'es-ES': 'Claro', - 'fr-FR': 'Clair', - 'nl-NL': 'Licht', - 'it-IT': 'Chiaro', - 'pl-PL': 'Jasny', - 'tr-TR': 'Açık', - 'ro-RO': 'Luminos', - 'el-GR': 'Φωτεινό', - 'ru-RU': 'Светлая', - }, - theme: themes.light + ...HightideSupportedThemeInformation.light, + theme: themes.light, }, dark: { - nameTranslations: { - 'de-DE': 'Dunkel', - 'en-US': 'Dark', - 'pt-BR': 'Escuro', - 'es-ES': 'Oscuro', - 'fr-FR': 'Sombre', - 'nl-NL': 'Donker', - 'it-IT': 'Scuro', - 'pl-PL': 'Ciemny', - 'tr-TR': 'Koyu', - 'ro-RO': 'Întunecat', - 'el-GR': 'Σκοτεινό', - 'ru-RU': 'Тёмная', - }, + ...HightideSupportedThemeInformation.dark, theme: themes.dark, }, } as const satisfies SupportedThemesConfig, diff --git a/packages/hightide-native/src/global-contexts/localization/index.ts b/packages/hightide-native/src/global-contexts/localization/index.ts index 066839af..7865a400 100644 --- a/packages/hightide-native/src/global-contexts/localization/index.ts +++ b/packages/hightide-native/src/global-contexts/localization/index.ts @@ -6,6 +6,7 @@ export { useDateTimeFormat, type LocalizationContextValue, type LocaleInformation, + type LocalizationConfig, type SupportedLocalesConfig, type Weekday, type CalendarType, diff --git a/packages/hightide-native/src/global-contexts/theme/index.ts b/packages/hightide-native/src/global-contexts/theme/index.ts index 3072fc0e..ef3ac27f 100644 --- a/packages/hightide-native/src/global-contexts/theme/index.ts +++ b/packages/hightide-native/src/global-contexts/theme/index.ts @@ -3,6 +3,7 @@ export * from './ThemeProvider' export { useCreateThemeConfig, type ThemeInformation, + type ThemeConfig, type ThemeConfigValue, type SystemTheme, type UseCreateThemeConfigProps, diff --git a/packages/hightide-native/src/hooks/useMultiSelect.ts b/packages/hightide-native/src/hooks/useMultiSelect.ts index ea153f92..685893b2 100644 --- a/packages/hightide-native/src/hooks/useMultiSelect.ts +++ b/packages/hightide-native/src/hooks/useMultiSelect.ts @@ -6,7 +6,7 @@ import { } from 'react' import { - useEventCallbackStabilizer, + useStableEvent, useListNavigation, useMultiSelection, useSearch, @@ -89,8 +89,8 @@ export function useMultiSelect({ isControlled: controlledValue !== undefined, }) - const editCompleteStable = useEventCallbackStabilizer(onEditComplete) - const onCloseStable = useEventCallbackStabilizer(onClose) + const editCompleteStable = useStableEvent(onEditComplete) + const onCloseStable = useStableEvent(onClose) const { searchResult: visibleOptions } = useSearch({ items: options, diff --git a/packages/hightide-native/src/hooks/useSelect.ts b/packages/hightide-native/src/hooks/useSelect.ts index add7549e..eca12568 100644 --- a/packages/hightide-native/src/hooks/useSelect.ts +++ b/packages/hightide-native/src/hooks/useSelect.ts @@ -6,7 +6,7 @@ import { } from 'react' import { - useEventCallbackStabilizer, + useStableEvent, useListNavigation, useSearch, useSingleSelection, @@ -76,10 +76,10 @@ export function useSelect({ const [isOpen, setIsOpen] = useState(initialIsOpen) const [searchQuery, setSearchQuery] = useState('') - const onValueChangeStable = useEventCallbackStabilizer(onValueChange) - const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete) - const onCloseStable = useEventCallbackStabilizer(onClose) - const onIsOpenChangeStable = useEventCallbackStabilizer(onIsOpenChange) + const onValueChangeStable = useStableEvent(onValueChange) + const onEditCompleteStable = useStableEvent(onEditComplete) + const onCloseStable = useStableEvent(onClose) + const onIsOpenChangeStable = useStableEvent(onIsOpenChange) const onSelectionChangeWrapper = useCallback((id: string | null) => { if (id === null) return diff --git a/packages/hightide-utils/CHANGELOG.md b/packages/hightide-utils/CHANGELOG.md index 9dfba192..0ab5f557 100644 --- a/packages/hightide-utils/CHANGELOG.md +++ b/packages/hightide-utils/CHANGELOG.md @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - unpublished + +### Added + +- `createFormStore` takes `initialValues`, `initialTouchedValues`, and a fixed `validation` list of `FormValidator` objects. Field errors are `string[]`. Async checks use `validationRunnerError` (`ValidationRunnerState`). `FormValidationUtils` groups checks by `string`, `number`, and `selection`, each with `mapTranslation`. `FormValidationErrorTypeUtils` lives in `utils/form-validation`. Validation messages use `validationError*` translation keys +- `useStateMachine` and `useStateMachineBinding` hooks +- Compiled translations for `el-GR`, `es-ES`, `fr-FR`, `it-IT`, `nl-NL`, `pl-PL`, `pt-BR`, `ro-RO`, `ru-RU`, and `tr-TR` +- `HightideSupportedLocaleInformation` and `HightideSupportedThemeInformation` shared locale and theme metadata +- `ThemeConfig`, `LocalizationConfig`, and `HightideConfig` config types +- `themeNameLight` and `themeNameDark` translation keys +- `useStateStoreBindings` commits one snapshot into a `useStateStore`. `onValueChange` returns `boolean | undefined`. `false` leaves that key out of the store write. `undefined` or `true` stores the proposed value. A later controlled value is written into the store without another callback. The controlled or uncontrolled mode stays fixed after the first render +- The `locales` folder is copied into `dist` during the build and exported as `@helpwave/hightide-utils/locales/*` +- `ThemeUtils.resolveThemeInformationLabel` for theme display names + +### Changed + +- `StateManagerProps` includes `children` +- Renamed `useEventCallbackStabilizer` to `useStableEvent` +- updated to `@helpwave/internationalization` to `0.5.0` +- Renamed `LocaleInformation.localName` to `localeName` +- `ThemeInformation` is `{ name, isDark, translationKey?, nameTranslations?, theme? }`. Labels prefer `translationKey` over `nameTranslations` +- Removed `defaultTimeZone` from `LocaleInformation`. Time zone still resolves from the controlled value, store, or `fallbackTimeZone` + +### Removed + +- `useStateMachineController`. A state machine reads a `useStateStore`, and the caller writes that store from `onStateChange` +- `EmailValidationUtils` and the standalone `validateEmail` export. Email checks go through `FormValidationUtils.email` + ## [0.1.9] - 2026-10-13 ### Added diff --git a/packages/hightide-utils/intl.config.json b/packages/hightide-utils/intl.config.json new file mode 100644 index 00000000..ec3a8f5b --- /dev/null +++ b/packages/hightide-utils/intl.config.json @@ -0,0 +1,22 @@ +{ + "supportedLocales": [ + "en-US", + "de-DE", + "el-GR", + "es-ES", + "fr-FR", + "it-IT", + "nl-NL", + "pl-PL", + "pt-BR", + "ro-RO", + "ru-RU", + "tr-TR" + ], + "compileTo": "typescript", + "name": "hightideTranslation", + "outputFile": "./src/i18n/translations.ts", + "translationFiles": [ + "./locales" + ] +} diff --git a/packages/hightide-utils/locales/de-DE.arb b/packages/hightide-utils/locales/de-DE.arb index c021a731..570e3167 100644 --- a/packages/hightide-utils/locales/de-DE.arb +++ b/packages/hightide-utils/locales/de-DE.arb @@ -1,4 +1,5 @@ { + "@@locale": "de-DE", "add": "Hinzufügen", "addFilter": "Filter hinzufügen", "addSorting": "Sortierung hinzufügen", @@ -15,6 +16,8 @@ "chooseLanguage": "Wähle deine bevorzugte Sprache", "chooseTheme": "Wähle dein bevorzugtes Farbschema.", "system": "System", + "themeNameLight": "Hell", + "themeNameDark": "Dunkel", "chooseSlide": "Wähle die angezeigte Slide aus", "clear": "Löschen", "removeProperty": "Eigenschaft entfernen", @@ -59,7 +62,7 @@ "maximumNumberOfFiles": "Maximale Anzahl Dateien: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { "type": "int" } } }, "allowedFileTypes": "Erlaubte Dateitypen: {types}", @@ -70,6 +73,53 @@ }, "menu": "Menü", "minLengthError": "Mindestlänge nicht erreicht.", + "validationErrorNotEmpty": "Das Feld darf nicht leer sein.", + "validationErrorInvalidEmail": "Die E-Mail ist ungültig.", + "validationErrorTooShort": "Der Wert muss mindestens {min} Zeichen enthalten.", + "@validationErrorTooShort": { + "placeholders": { + "min": { "type": "float" } + } + }, + "validationErrorTooLong": "Der Wert darf höchstens {max} Zeichen enthalten.", + "@validationErrorTooLong": { + "placeholders": { + "max": { "type": "float" } + } + }, + "validationErrorOutOfRangeString": "Der Wert muss zwischen {min} und {max} Zeichen lang sein.", + "@validationErrorOutOfRangeString": { + "placeholders": { + "min": { "type": "float" }, + "max": { "type": "float" } + } + }, + "validationErrorOutOfRangeNumber": "Der Wert muss zwischen {min} und {max} liegen.", + "@validationErrorOutOfRangeNumber": { + "placeholders": { + "min": { "type": "float" }, + "max": { "type": "float" } + } + }, + "validationErrorOutOfRangeSelectionItems": "Es müssen zwischen {min} und {max} Elemente ausgewählt werden.", + "@validationErrorOutOfRangeSelectionItems": { + "placeholders": { + "min": { "type": "float" }, + "max": { "type": "float" } + } + }, + "validationErrorTooFewSelectionItems": "Es müssen mindestens {min} Elemente ausgewählt werden.", + "@validationErrorTooFewSelectionItems": { + "placeholders": { + "min": { "type": "float" } + } + }, + "validationErrorTooManySelectionItems": "Es müssen maximal {max} Elemente ausgewählt werden.", + "@validationErrorTooManySelectionItems": { + "placeholders": { + "max": { "type": "float" } + } + }, "more": "Mehr", "next": "Weiter", "no": "Nein", @@ -81,13 +131,13 @@ "nResultsFound": "{count, plural, =1{# Ergebnis gefunden} other{# Ergebnisse gefunden}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { "type": "int" } } }, "nMoreFiles": "+ {count, plural, =1{# weitere Datei} other{# weitere Dateien}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { "type": "int" } } }, "of": "von", @@ -101,75 +151,75 @@ "rBetween": "Zwischen {value1} und {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { "type": "string" }, + "value2": { "type": "string" } } }, "rContains": "Enthält {value}", "@rContains": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rEndsWith": "Endet mit {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rEquals": "Gleich {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rGreaterThan": "Größer als {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rGreaterThanOrEqual": "Größer oder gleich {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rLessThan": "Kleiner als {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rLessThanOrEqual": "Kleiner oder gleich {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rNotBetween": "Nicht zwischen {value1} und {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { "type": "string" }, + "value2": { "type": "string" } } }, "rNotContains": "Enthält nicht {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rNotEquals": "Nicht gleich {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rStartsWith": "Beginnt mit {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "save": "Speichern", @@ -186,7 +236,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -195,10 +245,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -210,13 +260,13 @@ "pThemes": "{count, plural, =1{Farbschema} other{Farbschemas}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { "type": "int" } } }, "sDateTimeSelect": "{datetimeMode, select, time{Zeit auswählen} date{Datum auswählen} dateTime{Datum und Uhrzeit auswählen}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { "type": "string" } } }, "update": "Update", @@ -245,8 +295,6 @@ "playTrace": "Trace abspielen", "startDate": "Start", "startsWith": "Beginnt mit", - "notEmpty": "Das Feld darf nicht leer sein.", - "invalidEmail": "Die E-Mail ist ungültig.", "isFalse": "Ist falsch", "isTrue": "Ist wahr", "isUndefined": "Ist undefiniert", @@ -257,51 +305,6 @@ "before": "Vor", "onOrAfter": "Nach oder Gleich", "onOrBefore": "Vor oder Gleich", - "tooShort": "Der Wert muss mindestens {min} Zeichen enthalten.", - "@tooShort": { - "placeholders": { - "min": { "type": "number" } - } - }, - "tooLong": "Der Wert darf höchstens {max} Zeichen enthalten.", - "@tooLong": { - "placeholders": { - "max": { "type": "number" } - } - }, - "outOfRangeString": "Der Wert muss zwischen {min} und {max} Zeichen lang sein.", - "@outOfRangeString": { - "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } - } - }, - "outOfRangeNumber": "Der Wert muss zwischen {min} und {max} liegen.", - "@outOfRangeNumber": { - "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } - } - }, - "outOfRangeSelectionItems": "Es müssen zwischen {min} und {max} Elemente ausgewählt werden.", - "@outOfRangeSelectionItems": { - "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } - } - }, - "tooFewSelectionItems": "Es müssen mindestens {min} Elemente ausgewählt werden.", - "@tooFewSelectionItems": { - "placeholders": { - "min": { "type": "number" } - } - }, - "tooManySelectionItems": "Es müssen maximal {max} Elemente ausgewählt werden.", - "@tooManySelectionItems": { - "placeholders": { - "max": { "type": "number" } - } - }, "age": "Alter", "entryDate": "Eintragsdatum", "identifier": "Identifikator", @@ -312,7 +315,7 @@ "sGender": "{gender, select, male{Männlich} female{Weiblich} other{Divers}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { "type": "string" } } }, "goodToSeeYou": "Schön dich zu sehen", @@ -321,7 +324,7 @@ "rSortingOrderAfter": "Angewendet {otherSortings, plural, =0{als primäre Sortierung} =1{nach einer anderen Sortierung} other{nach # anderen Sortierungen}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { "type": "int" } } }, "columnPicker": "Spaltenauswahl", @@ -355,7 +358,7 @@ "sSortingState": "{sortDirection, select, asc{Sortierung (Aktuell Aufsteigend)} desc{Sortierung (Aktuell Absteigend)} other{Sortierung (Aktuell Keine)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { "type": "string" } } } } \ No newline at end of file diff --git a/packages/hightide-utils/locales/el-GR.arb b/packages/hightide-utils/locales/el-GR.arb index 35bb0288..8ab975c9 100644 --- a/packages/hightide-utils/locales/el-GR.arb +++ b/packages/hightide-utils/locales/el-GR.arb @@ -1,4 +1,5 @@ { + "@@locale": "el-GR", "add": "Προσθήκη", "addFilter": "Προσθήκη φίλτρου", "addSorting": "Προσθήκη ταξινόμησης", @@ -16,6 +17,8 @@ "chooseLanguage": "Επιλέξτε τη γλώσσα σας", "chooseTheme": "Επιλέξτε το θέμα χρωμάτων που προτιμάτε.", "system": "Σύστημα", + "themeNameLight": "Φωτεινό", + "themeNameDark": "Σκοτεινό", "chooseSlide": "Επιλέξτε διαφάνεια για εμφάνιση", "clear": "Εκκαθάριση", "removeProperty": "Αφαίρεση ιδιότητας", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Μέγιστος αριθμός αρχείων: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "Επιτρεπόμενοι τύποι αρχείων: {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Μενού", "minLengthError": "Δεν επιτεύχθηκε το ελάχιστο μήκος.", "more": "Περισσότερα", - "next": "Επόμενο", "no": "Όχι", "noColumn": "Καμία στήλη", "noData": "Δεν υπάρχουν δεδομένα", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =1{Βρέθηκε # αποτέλεσμα} other{Βρέθηκαν # αποτελέσματα}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =1{# ακόμη αρχείο} other{# ακόμη αρχεία}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "από", "optional": "Προαιρετικό", "pleaseWait": "Περιμένετε...", - "previous": "Προηγούμενο", "remove": "Αφαίρεση", "removeFilter": "Αφαίρεση φίλτρου", "required": "Υποχρεωτικό", @@ -102,75 +111,103 @@ "rBetween": "Μεταξύ {value1} και {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "Περιέχει {value}", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "Τελειώνει σε {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "Ίσο με {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "Μεγαλύτερο από {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "Μεγαλύτερο ή ίσο με {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "Μικρότερο από {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "Μικρότερο ή ίσο με {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "Όχι μεταξύ {value1} και {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "Δεν περιέχει {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "Διάφορο από {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "Ξεκινά με {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Αποθήκευση", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +248,17 @@ "pThemes": "{count, plural, =1{Θέμα} other{Θέματα}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "sDateTimeSelect": "{datetimeMode, select, time{Επιλογή ώρας} date{Επιλογή ημερομηνίας} dateTime{Επιλογή ημερομηνίας και ώρας}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Ενημέρωση", @@ -246,8 +287,8 @@ "playTrace": "Αναπαραγωγή ίχνους", "startDate": "Έναρξη", "startsWith": "Ξεκινά με", - "notEmpty": "Το πεδίο δεν μπορεί να είναι κενό.", - "invalidEmail": "Το email δεν είναι έγκυρο.", + "validationErrorNotEmpty": "Το πεδίο δεν μπορεί να είναι κενό.", + "validationErrorInvalidEmail": "Το email δεν είναι έγκυρο.", "isFalse": "Είναι ψευδές", "isTrue": "Είναι αληθές", "isUndefined": "Δεν έχει οριστεί", @@ -258,49 +299,69 @@ "before": "Πριν", "onOrAfter": "Μετά ή ίσο", "onOrBefore": "Πριν ή ίσο", - "tooShort": "Η τιμή απαιτεί τουλάχιστον {min} χαρακτήρες.", - "@tooShort": { + "validationErrorTooShort": "Η τιμή απαιτεί τουλάχιστον {min} χαρακτήρες.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "Η τιμή πρέπει να έχει λιγότερους από {max} χαρακτήρες.", - "@tooLong": { + "validationErrorTooLong": "Η τιμή πρέπει να έχει λιγότερους από {max} χαρακτήρες.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "Η τιμή πρέπει να έχει από {min} έως {max} χαρακτήρες.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "Η τιμή πρέπει να έχει από {min} έως {max} χαρακτήρες.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "Η τιμή πρέπει να είναι μεταξύ {min} και {max}.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "Η τιμή πρέπει να είναι μεταξύ {min} και {max}.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "Πρέπει να επιλεγούν από {min} έως {max} στοιχεία.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "Πρέπει να επιλεγούν από {min} έως {max} στοιχεία.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "Επιλέξτε τουλάχιστον {min} στοιχεία.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "Επιλέξτε τουλάχιστον {min} στοιχεία.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "Επιλέξτε το πολύ {max} στοιχεία.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "Επιλέξτε το πολύ {max} στοιχεία.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Ηλικία", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Άνδρας} female{Γυναίκα} other{Άλλο}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Χαιρόμαστε που σας βλέπουμε", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "Εφαρμόζεται {otherSortings, plural, =0{ως η πρώτη ταξινόμηση} =1{μετά από # άλλη ταξινόμηση} other{μετά από # άλλες ταξινομήσεις}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Επιλογέας στηλών", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Ταξινόμηση (τρέχουσα: αύξουσα)} desc{Ταξινόμηση (τρέχουσα: φθίνουσα)} other{Ταξινόμηση (τρέχουσα: καμία)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/en-US.arb b/packages/hightide-utils/locales/en-US.arb index af5ae481..e6c3894d 100644 --- a/packages/hightide-utils/locales/en-US.arb +++ b/packages/hightide-utils/locales/en-US.arb @@ -1,4 +1,5 @@ { + "@@locale": "en-US", "add": "Add", "addFilter": "Add filter", "addSorting": "Add sorting", @@ -16,6 +17,8 @@ "chooseLanguage": "Choose your language", "chooseTheme": "Choose your preferred color theme.", "system": "System", + "themeNameLight": "Light", + "themeNameDark": "Dark", "chooseSlide": "Choose slide to display", "clear": "Clear", "removeProperty": "Remove Property", @@ -60,7 +63,7 @@ "maximumNumberOfFiles": "Maximum number of files: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { "type": "int" } } }, "allowedFileTypes": "Allowed file types: {types}", @@ -71,6 +74,53 @@ }, "menu": "Menu", "minLengthError": "Minimum length not met.", + "validationErrorNotEmpty": "The field cannot be empty.", + "validationErrorInvalidEmail": "The email is not valid.", + "validationErrorTooShort": "The value requires at least {min} characters.", + "@validationErrorTooShort": { + "placeholders": { + "min": { "type": "float" } + } + }, + "validationErrorTooLong": "The value requires less than {max} characters.", + "@validationErrorTooLong": { + "placeholders": { + "max": { "type": "float" } + } + }, + "validationErrorOutOfRangeString": "The value needs to have between {min} and {max} characters.", + "@validationErrorOutOfRangeString": { + "placeholders": { + "min": { "type": "float" }, + "max": { "type": "float" } + } + }, + "validationErrorOutOfRangeNumber": "The value must be between {min} and {max}.", + "@validationErrorOutOfRangeNumber": { + "placeholders": { + "min": { "type": "float" }, + "max": { "type": "float" } + } + }, + "validationErrorOutOfRangeSelectionItems": "Between {min} and {max} items must be selected.", + "@validationErrorOutOfRangeSelectionItems": { + "placeholders": { + "min": { "type": "float" }, + "max": { "type": "float" } + } + }, + "validationErrorTooFewSelectionItems": "Select at least {min} items.", + "@validationErrorTooFewSelectionItems": { + "placeholders": { + "min": { "type": "float" } + } + }, + "validationErrorTooManySelectionItems": "Select at most {max} items.", + "@validationErrorTooManySelectionItems": { + "placeholders": { + "max": { "type": "float" } + } + }, "more": "More", "next": "Next", "no": "No", @@ -82,13 +132,13 @@ "nResultsFound": "{count, plural, =1{# result found} other{# results found}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { "type": "int" } } }, "nMoreFiles": "+ {count, plural, =1{# more file} other{# more files}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { "type": "int" } } }, "of": "of", @@ -102,75 +152,75 @@ "rBetween": "Between {value1} and {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { "type": "string" }, + "value2": { "type": "string" } } }, "rContains": "Contains {value}", "@rContains": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rEndsWith": "Ends with {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rEquals": "Equals {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rGreaterThan": "Greater than {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rGreaterThanOrEqual": "Greater than or equal {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rLessThan": "Less than {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rLessThanOrEqual": "Less than or equal {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rNotBetween": "Not between {value1} and {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { "type": "string" }, + "value2": { "type": "string" } } }, "rNotContains": "Not contains {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rNotEquals": "Not equals {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "rStartsWith": "Starts with {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { "type": "string" } } }, "save": "Save", @@ -187,7 +237,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +246,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +261,13 @@ "pThemes": "{count, plural, =1{Theme} other{Themes}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { "type": "int" } } }, "sDateTimeSelect": "{datetimeMode, select, time{Select Time} date{Select Date} dateTime{Select Date and Time}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { "type": "string" } } }, "update": "Update", @@ -246,8 +296,6 @@ "playTrace": "Play trace", "startDate": "Start", "startsWith": "Starts with", - "notEmpty": "The field cannot be empty.", - "invalidEmail": "The email is not valid.", "isFalse": "Is false", "isTrue": "Is true", "isUndefined": "Is undefined", @@ -258,51 +306,6 @@ "before": "Before", "onOrAfter": "After or Equal", "onOrBefore": "Before or Equal", - "tooShort": "The value requires at least {min} characters.", - "@tooShort": { - "placeholders": { - "min": { "type": "number" } - } - }, - "tooLong": "The value requires less than {max} characters.", - "@tooLong": { - "placeholders": { - "max": { "type": "number" } - } - }, - "outOfRangeString": "The value needs to have between {min} and {max} characters.", - "@outOfRangeString": { - "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } - } - }, - "outOfRangeNumber": "The value must be between {min} and {max}.", - "@outOfRangeNumber": { - "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } - } - }, - "outOfRangeSelectionItems": "Between {min} and {max} items must be selected.", - "@outOfRangeSelectionItems": { - "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } - } - }, - "tooFewSelectionItems": "Select at least {min} items.", - "@tooFewSelectionItems": { - "placeholders": { - "min": { "type": "number" } - } - }, - "tooManySelectionItems": "Select at most {max} items.", - "@tooManySelectionItems": { - "placeholders": { - "max": { "type": "number" } - } - }, "age": "Age", "entryDate": "Entry Date", "identifier": "Identifier", @@ -313,7 +316,7 @@ "sGender": "{gender, select, male{Male} female{Female} other{Other}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { "type": "string" } } }, "goodToSeeYou": "Good to see you", @@ -322,7 +325,7 @@ "rSortingOrderAfter": "Applied {otherSortings, plural, =0{as the first sorting} =1{after # other sorting} other{after # other sortings}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { "type": "int" } } }, "columnPicker": "Column Picker", @@ -355,7 +358,7 @@ "sSortingState": "{sortDirection, select, asc{Sorting (Currently Ascending)} desc{Sorting (Currently Descending)} other{Sorting (Currently None)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { "type": "string" } } } } \ No newline at end of file diff --git a/packages/hightide-utils/locales/es-ES.arb b/packages/hightide-utils/locales/es-ES.arb index b0890315..a2f382ca 100644 --- a/packages/hightide-utils/locales/es-ES.arb +++ b/packages/hightide-utils/locales/es-ES.arb @@ -1,4 +1,5 @@ { + "@@locale": "es-ES", "add": "Añadir", "addFilter": "Añadir filtro", "addSorting": "Añadir ordenación", @@ -16,6 +17,8 @@ "chooseLanguage": "Elige tu idioma", "chooseTheme": "Elige tu tema de color preferido.", "system": "Sistema", + "themeNameLight": "Claro", + "themeNameDark": "Oscuro", "chooseSlide": "Elige la diapositiva que quieres mostrar", "clear": "Borrar", "removeProperty": "Quitar propiedad", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Número máximo de archivos: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "Tipos de archivo permitidos: {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Menú", "minLengthError": "No se alcanza la longitud mínima.", "more": "Más", - "next": "Siguiente", "no": "No", "noColumn": "Ninguna columna", "noData": "Sin datos", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =1{# resultado encontrado} other{# resultados encontrados}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =1{# archivo más} other{# archivos más}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "de", "optional": "Opcional", "pleaseWait": "Espera un momento...", - "previous": "Anterior", "remove": "Quitar", "removeFilter": "Quitar filtro", "required": "Obligatorio", @@ -102,75 +111,103 @@ "rBetween": "Entre {value1} y {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "Contiene {value}", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "Termina en {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "Igual a {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "Mayor que {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "Mayor o igual que {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "Menor que {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "Menor o igual que {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "No está entre {value1} y {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "No contiene {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "Distinto de {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "Empieza por {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Guardar", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +248,17 @@ "pThemes": "{count, plural, =1{Tema} other{Temas}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "sDateTimeSelect": "{datetimeMode, select, time{Seleccionar hora} date{Seleccionar fecha} dateTime{Seleccionar fecha y hora}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Actualizar", @@ -246,8 +287,8 @@ "playTrace": "Reproducir traza", "startDate": "Inicio", "startsWith": "Empieza por", - "notEmpty": "El campo no puede estar vacío.", - "invalidEmail": "El correo electrónico no es válido.", + "validationErrorNotEmpty": "El campo no puede estar vacío.", + "validationErrorInvalidEmail": "El correo electrónico no es válido.", "isFalse": "Es falso", "isTrue": "Es verdadero", "isUndefined": "No está definido", @@ -258,49 +299,69 @@ "before": "Antes de", "onOrAfter": "En o después de", "onOrBefore": "En o antes de", - "tooShort": "El valor debe tener al menos {min} caracteres.", - "@tooShort": { + "validationErrorTooShort": "El valor debe tener al menos {min} caracteres.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "El valor debe tener menos de {max} caracteres.", - "@tooLong": { + "validationErrorTooLong": "El valor debe tener menos de {max} caracteres.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "El valor debe tener entre {min} y {max} caracteres.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "El valor debe tener entre {min} y {max} caracteres.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "El valor debe estar entre {min} y {max}.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "El valor debe estar entre {min} y {max}.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "Debes seleccionar entre {min} y {max} elementos.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "Debes seleccionar entre {min} y {max} elementos.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "Selecciona al menos {min} elementos.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "Selecciona al menos {min} elementos.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "Selecciona como máximo {max} elementos.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "Selecciona como máximo {max} elementos.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Edad", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Masculino} female{Femenino} other{Otro}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Nos alegra verte", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "Aplicada {otherSortings, plural, =0{como primera ordenación} =1{después de # ordenación previa} other{después de # ordenaciones previas}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Selector de columnas", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Ordenación (actualmente ascendente)} desc{Ordenación (actualmente descendente)} other{Ordenación (actualmente ninguna)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/fr-FR.arb b/packages/hightide-utils/locales/fr-FR.arb index 150436dd..d40e3fe7 100644 --- a/packages/hightide-utils/locales/fr-FR.arb +++ b/packages/hightide-utils/locales/fr-FR.arb @@ -1,4 +1,5 @@ { + "@@locale": "fr-FR", "add": "Ajouter", "addFilter": "Ajouter un filtre", "addSorting": "Ajouter un tri", @@ -12,10 +13,12 @@ "carousel": "Carrousel", "contains": "Contient", "change": "Modifier", - "changeColumnDisplay": "Modifier l'affichage des colonnes", + "changeColumnDisplay": "Modifier l''affichage des colonnes", "chooseLanguage": "Choisissez votre langue", "chooseTheme": "Choisissez votre thème de couleurs préféré.", "system": "Système", + "themeNameLight": "Clair", + "themeNameDark": "Sombre", "chooseSlide": "Choisir la diapositive à afficher", "clear": "Effacer", "removeProperty": "Supprimer la propriété", @@ -41,7 +44,7 @@ "editFilter": "Modifier le filtre", "enterText": "Saisissez du texte ici", "error": "Erreur", - "errorOccurred": "Une erreur s'est produite", + "errorOccurred": "Une erreur s''est produite", "exit": "Quitter", "filterOptions": "Options de filtre", "fieldRequiredError": "Ce champ est obligatoire.", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Nombre maximal de fichiers : {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "Types de fichiers autorisés : {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Menu", "minLengthError": "Longueur minimale non atteinte.", "more": "Plus", - "next": "Suivant", "no": "Non", "noColumn": "Aucune colonne", "noData": "Aucune donnée", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =1{# résultat trouvé} other{# résultats trouvés}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =1{# autre fichier} other{# autres fichiers}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "sur", "optional": "Facultatif", "pleaseWait": "Veuillez patienter...", - "previous": "Précédent", "remove": "Retirer", "removeFilter": "Retirer le filtre", "required": "Obligatoire", @@ -102,75 +111,103 @@ "rBetween": "Entre {value1} et {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "Contient {value}", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "Se termine par {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "Égal à {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "Supérieur à {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "Supérieur ou égal à {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "Inférieur à {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "Inférieur ou égal à {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "Pas entre {value1} et {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "Ne contient pas {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "Différent de {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "Commence par {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Enregistrer", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +248,17 @@ "pThemes": "{count, plural, =1{Thème} other{Thèmes}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, - "sDateTimeSelect": "{datetimeMode, select, time{Sélectionner l'heure} date{Sélectionner la date} dateTime{Sélectionner la date et l'heure}}", + "sDateTimeSelect": "{datetimeMode, select, time{Sélectionner l''heure} date{Sélectionner la date} dateTime{Sélectionner la date et l''heure}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Mettre à jour", @@ -246,11 +287,11 @@ "playTrace": "Lire la trace", "startDate": "Début", "startsWith": "Commence par", - "notEmpty": "Le champ ne peut pas être vide.", - "invalidEmail": "L'adresse e-mail n'est pas valide.", + "validationErrorNotEmpty": "Le champ ne peut pas être vide.", + "validationErrorInvalidEmail": "L''adresse e-mail n''est pas valide.", "isFalse": "Est faux", "isTrue": "Est vrai", - "isUndefined": "N'est pas défini", + "isUndefined": "N''est pas défini", "isNotUndefined": "Est défini", "lessThan": "Inférieur à", "lessThanOrEqual": "Inférieur ou égal à", @@ -258,53 +299,73 @@ "before": "Avant", "onOrAfter": "Le ou après", "onOrBefore": "Le ou avant", - "tooShort": "La valeur doit contenir au moins {min} caractères.", - "@tooShort": { + "validationErrorTooShort": "La valeur doit contenir au moins {min} caractères.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "La valeur doit contenir moins de {max} caractères.", - "@tooLong": { + "validationErrorTooLong": "La valeur doit contenir moins de {max} caractères.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "La valeur doit contenir entre {min} et {max} caractères.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "La valeur doit contenir entre {min} et {max} caractères.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "La valeur doit être comprise entre {min} et {max}.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "La valeur doit être comprise entre {min} et {max}.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "Vous devez sélectionner entre {min} et {max} éléments.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "Vous devez sélectionner entre {min} et {max} éléments.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "Sélectionnez au moins {min} éléments.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "Sélectionnez au moins {min} éléments.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "Sélectionnez au maximum {max} éléments.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "Sélectionnez au maximum {max} éléments.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Âge", - "entryDate": "Date d'entrée", + "entryDate": "Date d''entrée", "identifier": "Identifiant", "name": "Nom", "street": "Rue", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Masculin} female{Féminin} other{Autre}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Ravi de vous revoir", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "Appliqué {otherSortings, plural, =0{comme premier tri} =1{après # autre tri} other{après # autres tris}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Sélecteur de colonnes", @@ -340,7 +405,7 @@ "pinLeft": "Épingler à gauche", "pinRight": "Épingler à droite", "changeVisibility": "Modifier la visibilité", - "changePinning": "Modifier l'épinglage", + "changePinning": "Modifier l''épinglage", "changeSelection": "Modifier la sélection", "pinToRight": "Épingler à droite", "pinToLeft": "Épingler à gauche", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Tri (actuellement croissant)} desc{Tri (actuellement décroissant)} other{Tri (actuellement aucun)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/it-IT.arb b/packages/hightide-utils/locales/it-IT.arb index a60f5ed8..49135262 100644 --- a/packages/hightide-utils/locales/it-IT.arb +++ b/packages/hightide-utils/locales/it-IT.arb @@ -1,4 +1,5 @@ { + "@@locale": "it-IT", "add": "Aggiungi", "addFilter": "Aggiungi filtro", "addSorting": "Aggiungi ordinamento", @@ -16,6 +17,8 @@ "chooseLanguage": "Scegli la lingua", "chooseTheme": "Scegli il tema colore preferito.", "system": "Sistema", + "themeNameLight": "Chiaro", + "themeNameDark": "Scuro", "chooseSlide": "Scegli la slide da visualizzare", "clear": "Cancella", "removeProperty": "Rimuovi proprietà", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Numero massimo di file: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "Tipi di file consentiti: {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Menu", "minLengthError": "Lunghezza minima non raggiunta.", "more": "Altro", - "next": "Successivo", "no": "No", "noColumn": "Nessuna colonna", "noData": "Nessun dato", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =1{# risultato trovato} other{# risultati trovati}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =1{# altro file} other{altri # file}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "di", "optional": "Facoltativo", "pleaseWait": "Attendere...", - "previous": "Precedente", "remove": "Rimuovi", "removeFilter": "Rimuovi filtro", "required": "Obbligatorio", @@ -102,75 +111,103 @@ "rBetween": "Compreso tra {value1} e {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "Contiene {value}", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "Termina con {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "Uguale a {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "Maggiore di {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "Maggiore o uguale a {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "Minore di {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "Minore o uguale a {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "Non compreso tra {value1} e {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "Non contiene {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "Diverso da {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "Inizia con {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Salva", @@ -179,7 +216,7 @@ "searchResults": "Risultati della ricerca", "select": "Seleziona", "selection": "Selezione", - "selectOption": "Seleziona un'opzione", + "selectOption": "Seleziona un''opzione", "show": "Mostra", "showMore": "Mostra di più", "showLess": "Mostra di meno", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +248,17 @@ "pThemes": "{count, plural, =1{Tema} other{Temi}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "sDateTimeSelect": "{datetimeMode, select, time{Seleziona ora} date{Seleziona data} dateTime{Seleziona data e ora}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Aggiorna", @@ -246,8 +287,8 @@ "playTrace": "Riproduci traccia", "startDate": "Inizio", "startsWith": "Inizia con", - "notEmpty": "Il campo non può essere vuoto.", - "invalidEmail": "L'indirizzo email non è valido.", + "validationErrorNotEmpty": "Il campo non può essere vuoto.", + "validationErrorInvalidEmail": "L''indirizzo email non è valido.", "isFalse": "È falso", "isTrue": "È vero", "isUndefined": "Non è definito", @@ -258,49 +299,69 @@ "before": "Prima", "onOrAfter": "Uguale o successivo", "onOrBefore": "Uguale o precedente", - "tooShort": "Il valore deve contenere almeno {min} caratteri.", - "@tooShort": { + "validationErrorTooShort": "Il valore deve contenere almeno {min} caratteri.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "Il valore deve contenere meno di {max} caratteri.", - "@tooLong": { + "validationErrorTooLong": "Il valore deve contenere meno di {max} caratteri.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "Il valore deve contenere tra {min} e {max} caratteri.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "Il valore deve contenere tra {min} e {max} caratteri.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "Il valore deve essere compreso tra {min} e {max}.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "Il valore deve essere compreso tra {min} e {max}.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "Seleziona tra {min} e {max} elementi.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "Seleziona tra {min} e {max} elementi.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "Seleziona almeno {min} elementi.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "Seleziona almeno {min} elementi.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "Seleziona al massimo {max} elementi.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "Seleziona al massimo {max} elementi.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Età", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Maschio} female{Femmina} other{Altro}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Che piacere rivederti", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "Applicato {otherSortings, plural, =0{come primo ordinamento} =1{dopo # altro ordinamento} other{dopo altri # ordinamenti}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Selettore colonne", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Ordinamento (attualmente crescente)} desc{Ordinamento (attualmente decrescente)} other{Ordinamento (attualmente nessuno)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/nl-NL.arb b/packages/hightide-utils/locales/nl-NL.arb index 5a40487a..2e28aebd 100644 --- a/packages/hightide-utils/locales/nl-NL.arb +++ b/packages/hightide-utils/locales/nl-NL.arb @@ -1,4 +1,5 @@ { + "@@locale": "nl-NL", "add": "Toevoegen", "addFilter": "Filter toevoegen", "addSorting": "Sortering toevoegen", @@ -16,6 +17,8 @@ "chooseLanguage": "Kies je taal", "chooseTheme": "Kies je voorkeurskleurenthema.", "system": "Systeem", + "themeNameLight": "Licht", + "themeNameDark": "Donker", "chooseSlide": "Kies de dia die je wilt weergeven", "clear": "Wissen", "removeProperty": "Eigenschap verwijderen", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Maximaal aantal bestanden: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "Toegestane bestandstypen: {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Menu", "minLengthError": "Minimale lengte niet bereikt.", "more": "Meer", - "next": "Volgende", "no": "Nee", "noColumn": "Geen kolom", "noData": "Geen gegevens", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =1{# resultaat gevonden} other{# resultaten gevonden}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =1{# ander bestand} other{# andere bestanden}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "van", "optional": "Optioneel", "pleaseWait": "Even geduld...", - "previous": "Vorige", "remove": "Verwijderen", "removeFilter": "Filter verwijderen", "required": "Verplicht", @@ -102,75 +111,103 @@ "rBetween": "Tussen {value1} en {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "Bevat {value}", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "Eindigt op {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "Gelijk aan {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "Groter dan {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "Groter dan of gelijk aan {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "Kleiner dan {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "Kleiner dan of gelijk aan {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "Niet tussen {value1} en {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "Bevat geen {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "Niet gelijk aan {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "Begint met {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Opslaan", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -208,16 +245,20 @@ "submit": "Verzenden", "success": "Gelukt", "text": "Tekst", - "pThemes": "{count, plural, =1{Thema} other{Thema's}}", + "pThemes": "{count, plural, =1{Thema} other{Thema''s}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "sDateTimeSelect": "{datetimeMode, select, time{Tijd selecteren} date{Datum selecteren} dateTime{Datum en tijd selecteren}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Bijwerken", @@ -246,8 +287,8 @@ "playTrace": "Trace afspelen", "startDate": "Begin", "startsWith": "Begint met", - "notEmpty": "Het veld mag niet leeg zijn.", - "invalidEmail": "Het e-mailadres is ongeldig.", + "validationErrorNotEmpty": "Het veld mag niet leeg zijn.", + "validationErrorInvalidEmail": "Het e-mailadres is ongeldig.", "isFalse": "Is onwaar", "isTrue": "Is waar", "isUndefined": "Is niet gedefinieerd", @@ -258,49 +299,69 @@ "before": "Voor", "onOrAfter": "Op of na", "onOrBefore": "Op of voor", - "tooShort": "De waarde moet minimaal {min} tekens bevatten.", - "@tooShort": { + "validationErrorTooShort": "De waarde moet minimaal {min} tekens bevatten.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "De waarde moet minder dan {max} tekens bevatten.", - "@tooLong": { + "validationErrorTooLong": "De waarde moet minder dan {max} tekens bevatten.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "De waarde moet tussen {min} en {max} tekens bevatten.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "De waarde moet tussen {min} en {max} tekens bevatten.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "De waarde moet tussen {min} en {max} liggen.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "De waarde moet tussen {min} en {max} liggen.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "Er moeten tussen {min} en {max} items worden geselecteerd.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "Er moeten tussen {min} en {max} items worden geselecteerd.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "Selecteer minimaal {min} items.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "Selecteer minimaal {min} items.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "Selecteer maximaal {max} items.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "Selecteer maximaal {max} items.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Leeftijd", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Man} female{Vrouw} other{Anders}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Fijn je te zien", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "Toegepast {otherSortings, plural, =0{als eerste sortering} =1{na # andere sortering} other{na # andere sorteringen}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Kolomkiezer", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Sortering (momenteel oplopend)} desc{Sortering (momenteel aflopend)} other{Sortering (momenteel geen)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/pl-PL.arb b/packages/hightide-utils/locales/pl-PL.arb index 241ef42c..1b83d192 100644 --- a/packages/hightide-utils/locales/pl-PL.arb +++ b/packages/hightide-utils/locales/pl-PL.arb @@ -1,4 +1,5 @@ { + "@@locale": "pl-PL", "add": "Dodaj", "addFilter": "Dodaj filtr", "addSorting": "Dodaj sortowanie", @@ -16,6 +17,8 @@ "chooseLanguage": "Wybierz język", "chooseTheme": "Wybierz preferowany motyw kolorystyczny.", "system": "Systemowy", + "themeNameLight": "Jasny", + "themeNameDark": "Ciemny", "chooseSlide": "Wybierz slajd do wyświetlenia", "clear": "Wyczyść", "removeProperty": "Usuń właściwość", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Maksymalna liczba plików: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "Dozwolone typy plików: {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Menu", "minLengthError": "Nie osiągnięto minimalnej długości.", "more": "Więcej", - "next": "Następna", "no": "Nie", "noColumn": "Brak kolumny", "noData": "Brak danych", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =0{Znaleziono # wyników} =1{Znaleziono # wynik} =2{Znaleziono # wyniki} few{Znaleziono # wyniki} many{Znaleziono # wyników} other{Znaleziono # wyniki}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =0{# kolejnych plików} =1{# kolejny plik} =2{# kolejne pliki} few{# kolejne pliki} many{# kolejnych plików} other{# kolejne pliki}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "z", "optional": "Opcjonalne", "pleaseWait": "Proszę czekać...", - "previous": "Poprzednia", "remove": "Usuń", "removeFilter": "Usuń filtr", "required": "Wymagane", @@ -102,75 +111,103 @@ "rBetween": "Pomiędzy {value1} a {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "Zawiera {value}", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "Kończy się na {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "Równe {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "Większe niż {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "Większe lub równe {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "Mniejsze niż {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "Mniejsze lub równe {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "Nie pomiędzy {value1} a {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "Nie zawiera {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "Różne od {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "Zaczyna się od {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Zapisz", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +248,17 @@ "pThemes": "{count, plural, =1{Motyw} =2{Motywy} few{Motywy} many{Motywów} other{Motywy}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "sDateTimeSelect": "{datetimeMode, select, time{Wybierz godzinę} date{Wybierz datę} dateTime{Wybierz datę i godzinę}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Aktualizuj", @@ -246,8 +287,8 @@ "playTrace": "Odtwórz śledzenie", "startDate": "Początek", "startsWith": "Zaczyna się od", - "notEmpty": "Pole nie może być puste.", - "invalidEmail": "Adres e-mail jest nieprawidłowy.", + "validationErrorNotEmpty": "Pole nie może być puste.", + "validationErrorInvalidEmail": "Adres e-mail jest nieprawidłowy.", "isFalse": "Jest fałszem", "isTrue": "Jest prawdą", "isUndefined": "Jest niezdefiniowane", @@ -258,49 +299,69 @@ "before": "Przed", "onOrAfter": "W dniu lub po", "onOrBefore": "W dniu lub przed", - "tooShort": "Wartość musi mieć co najmniej {min} znaków.", - "@tooShort": { + "validationErrorTooShort": "Wartość musi mieć co najmniej {min} znaków.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "Wartość musi mieć mniej niż {max} znaków.", - "@tooLong": { + "validationErrorTooLong": "Wartość musi mieć mniej niż {max} znaków.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "Wartość musi mieć od {min} do {max} znaków.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "Wartość musi mieć od {min} do {max} znaków.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "Wartość musi mieścić się w przedziale od {min} do {max}.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "Wartość musi mieścić się w przedziale od {min} do {max}.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "Należy wybrać od {min} do {max} elementów.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "Należy wybrać od {min} do {max} elementów.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "Wybierz co najmniej {min} elementów.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "Wybierz co najmniej {min} elementów.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "Wybierz maksymalnie {max} elementów.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "Wybierz maksymalnie {max} elementów.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Wiek", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Mężczyzna} female{Kobieta} other{Inna}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Miło Cię widzieć", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "Zastosowano {otherSortings, plural, =0{jako pierwsze sortowanie} =1{po # innym sortowaniu} =2{po # innych sortowaniach} few{po # innych sortowaniach} many{po # innych sortowaniach} other{po # innych sortowaniach}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Wybór kolumn", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Sortowanie (obecnie rosnąco)} desc{Sortowanie (obecnie malejąco)} other{Sortowanie (obecnie brak)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/pt-BR.arb b/packages/hightide-utils/locales/pt-BR.arb index 6aab0302..b561ba10 100644 --- a/packages/hightide-utils/locales/pt-BR.arb +++ b/packages/hightide-utils/locales/pt-BR.arb @@ -1,4 +1,5 @@ { + "@@locale": "pt-BR", "add": "Adicionar", "addFilter": "Adicionar filtro", "addSorting": "Adicionar ordenação", @@ -16,6 +17,8 @@ "chooseLanguage": "Escolha seu idioma", "chooseTheme": "Escolha seu tema de cores preferido.", "system": "Sistema", + "themeNameLight": "Claro", + "themeNameDark": "Escuro", "chooseSlide": "Escolha o slide a ser exibido", "clear": "Limpar", "removeProperty": "Remover propriedade", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Número máximo de arquivos: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "Tipos de arquivo permitidos: {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Menu", "minLengthError": "Comprimento mínimo não atingido.", "more": "Mais", - "next": "Próximo", "no": "Não", "noColumn": "Nenhuma coluna", "noData": "Sem dados", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =1{# resultado encontrado} other{# resultados encontrados}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =1{# arquivo a mais} other{# arquivos a mais}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "de", "optional": "Opcional", "pleaseWait": "Aguarde...", - "previous": "Anterior", "remove": "Remover", "removeFilter": "Remover filtro", "required": "Obrigatório", @@ -102,75 +111,103 @@ "rBetween": "Entre {value1} e {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "Contém {value}", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "Termina com {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "Igual a {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "Maior que {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "Maior ou igual a {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "Menor que {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "Menor ou igual a {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "Não está entre {value1} e {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "Não contém {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "Diferente de {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "Começa com {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Salvar", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +248,17 @@ "pThemes": "{count, plural, =1{Tema} other{Temas}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "sDateTimeSelect": "{datetimeMode, select, time{Selecionar horário} date{Selecionar data} dateTime{Selecionar data e horário}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Atualizar", @@ -246,8 +287,8 @@ "playTrace": "Reproduzir rastreamento", "startDate": "Início", "startsWith": "Começa com", - "notEmpty": "O campo não pode ficar vazio.", - "invalidEmail": "O e-mail não é válido.", + "validationErrorNotEmpty": "O campo não pode ficar vazio.", + "validationErrorInvalidEmail": "O e-mail não é válido.", "isFalse": "É falso", "isTrue": "É verdadeiro", "isUndefined": "Não está definido", @@ -258,49 +299,69 @@ "before": "Antes de", "onOrAfter": "Em ou depois de", "onOrBefore": "Em ou antes de", - "tooShort": "O valor deve ter pelo menos {min} caracteres.", - "@tooShort": { + "validationErrorTooShort": "O valor deve ter pelo menos {min} caracteres.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "O valor deve ter menos de {max} caracteres.", - "@tooLong": { + "validationErrorTooLong": "O valor deve ter menos de {max} caracteres.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "O valor deve ter entre {min} e {max} caracteres.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "O valor deve ter entre {min} e {max} caracteres.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "O valor deve estar entre {min} e {max}.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "O valor deve estar entre {min} e {max}.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "Selecione entre {min} e {max} itens.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "Selecione entre {min} e {max} itens.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "Selecione pelo menos {min} itens.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "Selecione pelo menos {min} itens.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "Selecione no máximo {max} itens.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "Selecione no máximo {max} itens.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Idade", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Masculino} female{Feminino} other{Outro}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Que bom ver você", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "Aplicada {otherSortings, plural, =0{como a primeira ordenação} =1{após # outra ordenação} other{após # outras ordenações}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Seletor de colunas", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Ordenação (atualmente crescente)} desc{Ordenação (atualmente decrescente)} other{Ordenação (atualmente nenhuma)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/ro-RO.arb b/packages/hightide-utils/locales/ro-RO.arb index 5a7fc1a3..eeab20c8 100644 --- a/packages/hightide-utils/locales/ro-RO.arb +++ b/packages/hightide-utils/locales/ro-RO.arb @@ -1,4 +1,5 @@ { + "@@locale": "ro-RO", "add": "Adăugați", "addFilter": "Adăugați un filtru", "addSorting": "Adăugați o sortare", @@ -16,6 +17,8 @@ "chooseLanguage": "Alegeți limba", "chooseTheme": "Alegeți tema de culori preferată.", "system": "Sistem", + "themeNameLight": "Luminos", + "themeNameDark": "Întunecat", "chooseSlide": "Alegeți diapozitivul de afișat", "clear": "Ștergeți", "removeProperty": "Eliminați proprietatea", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Număr maxim de fișiere: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "Tipuri de fișiere permise: {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Meniu", "minLengthError": "Lungimea minimă nu a fost atinsă.", "more": "Mai mult", - "next": "Următor", "no": "Nu", "noColumn": "Nicio coloană", "noData": "Nu există date", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =1{# rezultat găsit} other{Rezultate găsite: #}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =1{încă # fișier} other{încă # fișiere}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "din", "optional": "Opțional", "pleaseWait": "Vă rugăm să așteptați...", - "previous": "Anterior", "remove": "Eliminați", "removeFilter": "Eliminați filtrul", "required": "Obligatoriu", @@ -102,75 +111,103 @@ "rBetween": "Între {value1} și {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "Conține {value}", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "Se termină cu {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "Egal cu {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "Mai mare decât {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "Mai mare sau egal cu {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "Mai mic decât {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "Mai mic sau egal cu {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "Nu este între {value1} și {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "Nu conține {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "Diferit de {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "Începe cu {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Salvați", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +248,17 @@ "pThemes": "{count, plural, =1{Temă} other{Teme}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "sDateTimeSelect": "{datetimeMode, select, time{Selectați ora} date{Selectați data} dateTime{Selectați data și ora}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Actualizați", @@ -246,8 +287,8 @@ "playTrace": "Redați urmărirea", "startDate": "Început", "startsWith": "Începe cu", - "notEmpty": "Câmpul nu poate fi gol.", - "invalidEmail": "Adresa de e-mail nu este validă.", + "validationErrorNotEmpty": "Câmpul nu poate fi gol.", + "validationErrorInvalidEmail": "Adresa de e-mail nu este validă.", "isFalse": "Este fals", "isTrue": "Este adevărat", "isUndefined": "Este nedefinit", @@ -258,49 +299,69 @@ "before": "Înainte", "onOrAfter": "După sau egal", "onOrBefore": "Înainte sau egal", - "tooShort": "Valoarea trebuie să aibă cel puțin {min} caractere.", - "@tooShort": { + "validationErrorTooShort": "Valoarea trebuie să aibă cel puțin {min} caractere.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "Valoarea trebuie să aibă mai puțin de {max} caractere.", - "@tooLong": { + "validationErrorTooLong": "Valoarea trebuie să aibă mai puțin de {max} caractere.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "Valoarea trebuie să aibă între {min} și {max} caractere.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "Valoarea trebuie să aibă între {min} și {max} caractere.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "Valoarea trebuie să fie între {min} și {max}.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "Valoarea trebuie să fie între {min} și {max}.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "Trebuie selectate între {min} și {max} elemente.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "Trebuie selectate între {min} și {max} elemente.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "Selectați cel puțin {min} elemente.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "Selectați cel puțin {min} elemente.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "Selectați cel mult {max} elemente.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "Selectați cel mult {max} elemente.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Vârstă", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Masculin} female{Feminin} other{Altul}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Ne bucurăm să vă revedem", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "Aplicată {otherSortings, plural, =0{ca primă sortare} =1{după o altă sortare} other{după alte sortări (#)}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Selector de coloane", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Sortare (în prezent crescătoare)} desc{Sortare (în prezent descrescătoare)} other{Sortare (în prezent niciuna)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/ru-RU.arb b/packages/hightide-utils/locales/ru-RU.arb index 2388e63d..e5d4a67d 100644 --- a/packages/hightide-utils/locales/ru-RU.arb +++ b/packages/hightide-utils/locales/ru-RU.arb @@ -1,4 +1,5 @@ { + "@@locale": "ru-RU", "add": "Добавить", "addFilter": "Добавить фильтр", "addSorting": "Добавить сортировку", @@ -16,6 +17,8 @@ "chooseLanguage": "Выберите язык", "chooseTheme": "Выберите предпочитаемую цветовую тему.", "system": "Системная", + "themeNameLight": "Светлая", + "themeNameDark": "Тёмная", "chooseSlide": "Выберите слайд для отображения", "clear": "Очистить", "removeProperty": "Удалить свойство", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Максимальное количество файлов: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "Допустимые типы файлов: {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Меню", "minLengthError": "Не достигнута минимальная длина.", "more": "Ещё", - "next": "Следующая", "no": "Нет", "noColumn": "Нет столбца", "noData": "Нет данных", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =0{Найдено # результатов} =1{Найден # результат} =2{Найдено # результата} few{Найдено # результата} many{Найдено # результатов} other{Найдено # результата}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =0{ещё # файлов} =1{ещё # файл} =2{ещё # файла} few{ещё # файла} many{ещё # файлов} other{ещё # файла}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "из", "optional": "Необязательно", "pleaseWait": "Пожалуйста, подождите...", - "previous": "Предыдущая", "remove": "Удалить", "removeFilter": "Удалить фильтр", "required": "Обязательно", @@ -102,75 +111,103 @@ "rBetween": "Между {value1} и {value2}", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "Содержит {value}", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "Заканчивается на {value}", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "Равно {value}", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "Больше {value}", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "Больше или равно {value}", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "Меньше {value}", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "Меньше или равно {value}", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "Не между {value1} и {value2}", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "Не содержит {value}", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "Не равно {value}", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "Начинается с {value}", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Сохранить", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +248,17 @@ "pThemes": "{count, plural, =1{Тема} =2{Темы} few{Темы} many{Тем} other{Темы}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "sDateTimeSelect": "{datetimeMode, select, time{Выберите время} date{Выберите дату} dateTime{Выберите дату и время}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Обновить", @@ -246,8 +287,8 @@ "playTrace": "Воспроизвести трассировку", "startDate": "Начало", "startsWith": "Начинается с", - "notEmpty": "Поле не может быть пустым.", - "invalidEmail": "Некорректный адрес электронной почты.", + "validationErrorNotEmpty": "Поле не может быть пустым.", + "validationErrorInvalidEmail": "Некорректный адрес электронной почты.", "isFalse": "Ложно", "isTrue": "Истинно", "isUndefined": "Не определено", @@ -258,49 +299,69 @@ "before": "До", "onOrAfter": "Не ранее", "onOrBefore": "Не позднее", - "tooShort": "Значение должно содержать не менее {min} символов.", - "@tooShort": { + "validationErrorTooShort": "Значение должно содержать не менее {min} символов.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "Значение должно содержать менее {max} символов.", - "@tooLong": { + "validationErrorTooLong": "Значение должно содержать менее {max} символов.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "Значение должно содержать от {min} до {max} символов.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "Значение должно содержать от {min} до {max} символов.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "Значение должно быть в диапазоне от {min} до {max}.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "Значение должно быть в диапазоне от {min} до {max}.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "Необходимо выбрать от {min} до {max} элементов.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "Необходимо выбрать от {min} до {max} элементов.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "Выберите не менее {min} элементов.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "Выберите не менее {min} элементов.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "Выберите не более {max} элементов.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "Выберите не более {max} элементов.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Возраст", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Мужской} female{Женский} other{Другой}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Рады вас видеть", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "Применяется {otherSortings, plural, =0{как первая сортировка} =1{после # другой сортировки} =2{после # других сортировок} few{после # других сортировок} many{после # других сортировок} other{после # других сортировок}}", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Выбор столбцов", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Сортировка (сейчас по возрастанию)} desc{Сортировка (сейчас по убыванию)} other{Сортировка (сейчас отключена)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/de-DE.arb b/packages/hightide-utils/locales/time/de-DE.arb index dc4b354b..d57b5aa8 100644 --- a/packages/hightide-utils/locales/time/de-DE.arb +++ b/packages/hightide-utils/locales/time/de-DE.arb @@ -1,4 +1,6 @@ { + "@@locale": "de-DE", + "@@prefix": "time", "january": "Januar", "february": "Febuar", "march": "März", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,20 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Januar} february{Februar} march{März} april{April} may{Mai} june{Juni} july{Juli} august{August} september{September} october{Oktober} november{November} december{Dezember} other{Unbekannter Monat}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { "type": "string" } } }, "sMonthShort": "{month, select, january{Jan} february{Feb} march{Mär} april{Apr} may{Mai} june{Jun} july{Jul} august{Aug} september{Sep} october{Okt} november{Nov} december{Dez} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { "type": "string" } } } } \ No newline at end of file diff --git a/packages/hightide-utils/locales/time/el-GR.arb b/packages/hightide-utils/locales/time/el-GR.arb index 0e7b5902..cc01df57 100644 --- a/packages/hightide-utils/locales/time/el-GR.arb +++ b/packages/hightide-utils/locales/time/el-GR.arb @@ -1,4 +1,6 @@ { + "@@locale": "el-GR", + "@@prefix": "time", "january": "Ιανουάριος", "february": "Φεβρουάριος", "march": "Μάρτιος", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Ιανουάριος} february{Φεβρουάριος} march{Μάρτιος} april{Απρίλιος} may{Μάιος} june{Ιούνιος} july{Ιούλιος} august{Αύγουστος} september{Σεπτέμβριος} october{Οκτώβριος} november{Νοέμβριος} december{Δεκέμβριος} other{Άγνωστος μήνας}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{Ιαν} february{Φεβ} march{Μαρ} april{Απρ} may{Μάι} june{Ιουν} july{Ιουλ} august{Αυγ} september{Σεπ} october{Οκτ} november{Νοε} december{Δεκ} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/en-US.arb b/packages/hightide-utils/locales/time/en-US.arb index 0bb48760..c5e32131 100644 --- a/packages/hightide-utils/locales/time/en-US.arb +++ b/packages/hightide-utils/locales/time/en-US.arb @@ -1,4 +1,6 @@ { + "@@locale": "en-US", + "@@prefix": "time", "january": "January", "february": "Febuary", "march": "March", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,20 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{January} february{February} march{March} april{April} may{May} june{June} july{July} august{August} september{September} october{October} november{November} december{December} other{Unknown Month}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { "type": "string" } } }, "sMonthShort": "{month, select, january{Jan} february{Feb} march{Mar} april{Apr} may{May} june{Jun} july{Jul} august{Aug} september{Sep} october{Oct} november{Nov} december{Dec} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { "type": "string" } } } } \ No newline at end of file diff --git a/packages/hightide-utils/locales/time/es-ES.arb b/packages/hightide-utils/locales/time/es-ES.arb index e3210b90..e0193d68 100644 --- a/packages/hightide-utils/locales/time/es-ES.arb +++ b/packages/hightide-utils/locales/time/es-ES.arb @@ -1,4 +1,6 @@ { + "@@locale": "es-ES", + "@@prefix": "time", "january": "Enero", "february": "Febrero", "march": "Marzo", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Enero} february{Febrero} march{Marzo} april{Abril} may{Mayo} june{Junio} july{Julio} august{Agosto} september{Septiembre} october{Octubre} november{Noviembre} december{Diciembre} other{Mes desconocido}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{Ene} february{Feb} march{Mar} april{Abr} may{May} june{Jun} july{Jul} august{Ago} september{Sept} october{Oct} november{Nov} december{Dic} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/fr-FR.arb b/packages/hightide-utils/locales/time/fr-FR.arb index 33e67476..4e3b3955 100644 --- a/packages/hightide-utils/locales/time/fr-FR.arb +++ b/packages/hightide-utils/locales/time/fr-FR.arb @@ -1,4 +1,6 @@ { + "@@locale": "fr-FR", + "@@prefix": "time", "january": "Janvier", "february": "Février", "march": "Mars", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,12 +99,12 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, "yesterday": "Hier", - "today": "Aujourd'hui", + "today": "Aujourd''hui", "tomorrow": "Demain", "in": "dans", "ago": "il y a", @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Janvier} february{Février} march{Mars} april{Avril} may{Mai} june{Juin} july{Juillet} august{Août} september{Septembre} october{Octobre} november{Novembre} december{Décembre} other{Mois inconnu}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{janv.} february{févr.} march{mars} april{avr.} may{mai} june{juin} july{juil.} august{août} september{sept.} october{oct.} november{nov.} december{déc.} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/it-IT.arb b/packages/hightide-utils/locales/time/it-IT.arb index f0f413a1..be7493a8 100644 --- a/packages/hightide-utils/locales/time/it-IT.arb +++ b/packages/hightide-utils/locales/time/it-IT.arb @@ -1,4 +1,6 @@ { + "@@locale": "it-IT", + "@@prefix": "time", "january": "Gennaio", "february": "Febbraio", "march": "Marzo", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Gennaio} february{Febbraio} march{Marzo} april{Aprile} may{Maggio} june{Giugno} july{Luglio} august{Agosto} september{Settembre} october{Ottobre} november{Novembre} december{Dicembre} other{Mese sconosciuto}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{Gen} february{Feb} march{Mar} april{Apr} may{Mag} june{Giu} july{Lug} august{Ago} september{Set} october{Ott} november{Nov} december{Dic} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/nl-NL.arb b/packages/hightide-utils/locales/time/nl-NL.arb index f714820d..d2de4a5f 100644 --- a/packages/hightide-utils/locales/time/nl-NL.arb +++ b/packages/hightide-utils/locales/time/nl-NL.arb @@ -1,4 +1,6 @@ { + "@@locale": "nl-NL", + "@@prefix": "time", "january": "Januari", "february": "Februari", "march": "Maart", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Januari} february{Februari} march{Maart} april{April} may{Mei} june{Juni} july{Juli} august{Augustus} september{September} october{Oktober} november{November} december{December} other{Onbekende maand}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{jan} february{feb} march{mrt} april{apr} may{mei} june{jun} july{jul} august{aug} september{sep} october{okt} november{nov} december{dec} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/pl-PL.arb b/packages/hightide-utils/locales/time/pl-PL.arb index 616e6b25..6963b05e 100644 --- a/packages/hightide-utils/locales/time/pl-PL.arb +++ b/packages/hightide-utils/locales/time/pl-PL.arb @@ -1,4 +1,6 @@ { + "@@locale": "pl-PL", + "@@prefix": "time", "january": "Styczeń", "february": "Luty", "march": "Marzec", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Styczeń} february{Luty} march{Marzec} april{Kwiecień} may{Maj} june{Czerwiec} july{Lipiec} august{Sierpień} september{Wrzesień} october{Październik} november{Listopad} december{Grudzień} other{Nieznany miesiąc}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{Sty} february{Lut} march{Mar} april{Kwi} may{Maj} june{Cze} july{Lip} august{Sie} september{Wrz} october{Paź} november{Lis} december{Gru} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/pt-BR.arb b/packages/hightide-utils/locales/time/pt-BR.arb index 953e3fbe..c7508004 100644 --- a/packages/hightide-utils/locales/time/pt-BR.arb +++ b/packages/hightide-utils/locales/time/pt-BR.arb @@ -1,4 +1,6 @@ { + "@@locale": "pt-BR", + "@@prefix": "time", "january": "Janeiro", "february": "Fevereiro", "march": "Março", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Janeiro} february{Fevereiro} march{Março} april{Abril} may{Maio} june{Junho} july{Julho} august{Agosto} september{Setembro} october{Outubro} november{Novembro} december{Dezembro} other{Mês desconhecido}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{Jan} february{Fev} march{Mar} april{Abr} may{Mai} june{Jun} july{Jul} august{Ago} september{Set} october{Out} november{Nov} december{Dez} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/ro-RO.arb b/packages/hightide-utils/locales/time/ro-RO.arb index cac3657d..415b67de 100644 --- a/packages/hightide-utils/locales/time/ro-RO.arb +++ b/packages/hightide-utils/locales/time/ro-RO.arb @@ -1,4 +1,6 @@ { + "@@locale": "ro-RO", + "@@prefix": "time", "january": "Ianuarie", "february": "Februarie", "march": "Martie", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Ianuarie} february{Februarie} march{Martie} april{Aprilie} may{Mai} june{Iunie} july{Iulie} august{August} september{Septembrie} october{Octombrie} november{Noiembrie} december{Decembrie} other{Lună necunoscută}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{Ian.} february{Feb.} march{Mar.} april{Apr.} may{Mai} june{Iun.} july{Iul.} august{Aug.} september{Sept.} october{Oct.} november{Nov.} december{Dec.} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/ru-RU.arb b/packages/hightide-utils/locales/time/ru-RU.arb index 54499187..9c96fd14 100644 --- a/packages/hightide-utils/locales/time/ru-RU.arb +++ b/packages/hightide-utils/locales/time/ru-RU.arb @@ -1,4 +1,6 @@ { + "@@locale": "ru-RU", + "@@prefix": "time", "january": "Январь", "february": "Февраль", "march": "Март", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Январь} february{Февраль} march{Март} april{Апрель} may{Май} june{Июнь} july{Июль} august{Август} september{Сентябрь} october{Октябрь} november{Ноябрь} december{Декабрь} other{Неизвестный месяц}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{Янв} february{Фев} march{Мар} april{Апр} may{Май} june{Июн} july{Июл} august{Авг} september{Сен} october{Окт} november{Ноя} december{Дек} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/time/tr-TR.arb b/packages/hightide-utils/locales/time/tr-TR.arb index 69c667ba..301c791b 100644 --- a/packages/hightide-utils/locales/time/tr-TR.arb +++ b/packages/hightide-utils/locales/time/tr-TR.arb @@ -1,4 +1,6 @@ { + "@@locale": "tr-TR", + "@@prefix": "time", "january": "Ocak", "february": "Şubat", "march": "Mart", @@ -15,7 +17,7 @@ "@century": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -23,7 +25,7 @@ "@decade": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -31,7 +33,7 @@ "@year": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -39,7 +41,7 @@ "@month": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -49,7 +51,7 @@ "@day": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -57,7 +59,7 @@ "@hour": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -65,7 +67,7 @@ "@minute": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -73,7 +75,7 @@ "@second": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -81,7 +83,7 @@ "@millisecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -89,7 +91,7 @@ "@microsecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -97,7 +99,7 @@ "@nanosecond": { "placeholders": { "count": { - "type": "number" + "type": "int" } } }, @@ -110,7 +112,7 @@ "@inDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, @@ -118,20 +120,24 @@ "@agoDays": { "placeholders": { "days": { - "type": "number" + "type": "int" } } }, "sMonthLong": "{month, select, january{Ocak} february{Şubat} march{Mart} april{Nisan} may{Mayıs} june{Haziran} july{Temmuz} august{Ağustos} september{Eylül} october{Ekim} november{Kasım} december{Aralık} other{Bilinmeyen ay}}", "@sMonthLong": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } }, "sMonthShort": "{month, select, january{Oca} february{Şub} march{Mar} april{Nis} may{May} june{Haz} july{Tem} august{Ağu} september{Eyl} october{Eki} november{Kas} december{Ara} other{???}}", "@sMonthShort": { "placeholders": { - "month": {} + "month": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/locales/tr-TR.arb b/packages/hightide-utils/locales/tr-TR.arb index 3a4160eb..32e59300 100644 --- a/packages/hightide-utils/locales/tr-TR.arb +++ b/packages/hightide-utils/locales/tr-TR.arb @@ -1,4 +1,5 @@ { + "@@locale": "tr-TR", "add": "Ekle", "addFilter": "Filtre ekle", "addSorting": "Sıralama ekle", @@ -16,6 +17,8 @@ "chooseLanguage": "Dilinizi seçin", "chooseTheme": "Tercih ettiğiniz renk temasını seçin.", "system": "Sistem", + "themeNameLight": "Açık", + "themeNameDark": "Koyu", "chooseSlide": "Gösterilecek slaytı seçin", "clear": "Temizle", "removeProperty": "Özelliği kaldır", @@ -60,19 +63,22 @@ "maximumNumberOfFiles": "Maksimum dosya sayısı: {count}", "@maximumNumberOfFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "allowedFileTypes": "İzin verilen dosya türleri: {types}", "@allowedFileTypes": { "placeholders": { - "types": { "type": "string" } + "types": { + "type": "string" + } } }, "menu": "Menü", "minLengthError": "Minimum uzunluğa ulaşılmadı.", "more": "Daha fazla", - "next": "Sonraki", "no": "Hayır", "noColumn": "Sütun yok", "noData": "Veri yok", @@ -82,19 +88,22 @@ "nResultsFound": "{count, plural, =1{# sonuç bulundu} other{# sonuç bulundu}}", "@nResultsFound": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "nMoreFiles": "+ {count, plural, =1{# dosya daha} other{# dosya daha}}", "@nMoreFiles": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "of": "/", "optional": "İsteğe bağlı", "pleaseWait": "Lütfen bekleyin...", - "previous": "Önceki", "remove": "Kaldır", "removeFilter": "Filtreyi kaldır", "required": "Zorunlu", @@ -102,75 +111,103 @@ "rBetween": "{value1} ile {value2} arasında", "@rBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rContains": "{value} içerir", "@rContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEndsWith": "{value} ile biter", "@rEndsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rEquals": "{value} değerine eşit", "@rEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThan": "{value} değerinden büyük", "@rGreaterThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rGreaterThanOrEqual": "{value} değerinden büyük veya eşit", "@rGreaterThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThan": "{value} değerinden küçük", "@rLessThan": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rLessThanOrEqual": "{value} değerinden küçük veya eşit", "@rLessThanOrEqual": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotBetween": "{value1} ile {value2} arasında değil", "@rNotBetween": { "placeholders": { - "value1": {}, - "value2": {} + "value1": { + "type": "string" + }, + "value2": { + "type": "string" + } } }, "rNotContains": "{value} içermez", "@rNotContains": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rNotEquals": "{value} değerine eşit değil", "@rNotEquals": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "rStartsWith": "{value} ile başlar", "@rStartsWith": { "placeholders": { - "value": {} + "value": { + "type": "string" + } } }, "save": "Kaydet", @@ -187,7 +224,7 @@ "@showSlide": { "placeholders": { "index": { - "type": "number" + "type": "int" } } }, @@ -196,10 +233,10 @@ "@slideOf": { "placeholders": { "index": { - "type": "number" + "type": "int" }, "length": { - "type": "number" + "type": "int" } } }, @@ -211,13 +248,17 @@ "pThemes": "{count, plural, =1{Tema} other{Temalar}}", "@pThemes": { "placeholders": { - "count": { "type": "number" } + "count": { + "type": "int" + } } }, "sDateTimeSelect": "{datetimeMode, select, time{Saat seçin} date{Tarih seçin} dateTime{Tarih ve saat seçin}}", "@sDateTimeSelect": { "placeholders": { - "datetimeMode": {} + "datetimeMode": { + "type": "string" + } } }, "update": "Güncelle", @@ -246,8 +287,8 @@ "playTrace": "İzlemeyi oynat", "startDate": "Başlangıç", "startsWith": "İle başlar", - "notEmpty": "Bu alan boş bırakılamaz.", - "invalidEmail": "E-posta adresi geçerli değil.", + "validationErrorNotEmpty": "Bu alan boş bırakılamaz.", + "validationErrorInvalidEmail": "E-posta adresi geçerli değil.", "isFalse": "Yanlış", "isTrue": "Doğru", "isUndefined": "Tanımsız", @@ -258,49 +299,69 @@ "before": "Önce", "onOrAfter": "Sonra veya eşit", "onOrBefore": "Önce veya eşit", - "tooShort": "Değer en az {min} karakter içermelidir.", - "@tooShort": { + "validationErrorTooShort": "Değer en az {min} karakter içermelidir.", + "@validationErrorTooShort": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooLong": "Değer {max} karakterden az olmalıdır.", - "@tooLong": { + "validationErrorTooLong": "Değer {max} karakterden az olmalıdır.", + "@validationErrorTooLong": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, - "outOfRangeString": "Değer {min} ile {max} karakter arasında olmalıdır.", - "@outOfRangeString": { + "validationErrorOutOfRangeString": "Değer {min} ile {max} karakter arasında olmalıdır.", + "@validationErrorOutOfRangeString": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeNumber": "Değer {min} ile {max} arasında olmalıdır.", - "@outOfRangeNumber": { + "validationErrorOutOfRangeNumber": "Değer {min} ile {max} arasında olmalıdır.", + "@validationErrorOutOfRangeNumber": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "outOfRangeSelectionItems": "{min} ile {max} arasında öğe seçilmelidir.", - "@outOfRangeSelectionItems": { + "validationErrorOutOfRangeSelectionItems": "{min} ile {max} arasında öğe seçilmelidir.", + "@validationErrorOutOfRangeSelectionItems": { "placeholders": { - "min": { "type": "number" }, - "max": { "type": "number" } + "min": { + "type": "float" + }, + "max": { + "type": "float" + } } }, - "tooFewSelectionItems": "En az {min} öğe seçin.", - "@tooFewSelectionItems": { + "validationErrorTooFewSelectionItems": "En az {min} öğe seçin.", + "@validationErrorTooFewSelectionItems": { "placeholders": { - "min": { "type": "number" } + "min": { + "type": "float" + } } }, - "tooManySelectionItems": "En fazla {max} öğe seçin.", - "@tooManySelectionItems": { + "validationErrorTooManySelectionItems": "En fazla {max} öğe seçin.", + "@validationErrorTooManySelectionItems": { "placeholders": { - "max": { "type": "number" } + "max": { + "type": "float" + } } }, "age": "Yaş", @@ -313,7 +374,9 @@ "sGender": "{gender, select, male{Erkek} female{Kadın} other{Diğer}}", "@sGender": { "placeholders": { - "gender": {} + "gender": { + "type": "string" + } } }, "goodToSeeYou": "Sizi görmek güzel", @@ -322,7 +385,9 @@ "rSortingOrderAfter": "{otherSortings, plural, =0{İlk sıralama olarak} =1{# diğer sıralamadan sonra} other{# diğer sıralamadan sonra}} uygulandı", "@rSortingOrderAfter": { "placeholders": { - "otherSortings": { "type": "number" } + "otherSortings": { + "type": "int" + } } }, "columnPicker": "Sütun seçici", @@ -355,7 +420,9 @@ "sSortingState": "{sortDirection, select, asc{Sıralama (Şu an artan)} desc{Sıralama (Şu an azalan)} other{Sıralama (Şu an yok)}}", "@sSortingState": { "placeholders": { - "sortDirection": {} + "sortDirection": { + "type": "string" + } } } -} \ No newline at end of file +} diff --git a/packages/hightide-utils/package.json b/packages/hightide-utils/package.json index cbff3b76..47e93969 100644 --- a/packages/hightide-utils/package.json +++ b/packages/hightide-utils/package.json @@ -10,7 +10,7 @@ "access": "public" }, "license": "AGPL-3.0-only", - "version": "0.1.9", + "version": "0.2.0", "type": "module", "files": [ "dist", @@ -42,6 +42,11 @@ "require": "./dist/context/debug/index.cjs", "import": "./dist/context/debug/index.js" }, + "./form": { + "types": "./dist/form/index.d.ts", + "require": "./dist/form/index.cjs", + "import": "./dist/form/index.js" + }, "./hooks": { "types": "./dist/hooks/index.d.ts", "require": "./dist/hooks/index.cjs", @@ -52,26 +57,33 @@ "require": "./dist/i18n/index.cjs", "import": "./dist/i18n/index.js" }, + "./interfaces": { + "types": "./dist/interfaces/index.d.ts", + "require": "./dist/interfaces/index.cjs", + "import": "./dist/interfaces/index.js" + }, "./utils": { "types": "./dist/utils/index.d.ts", "require": "./dist/utils/index.cjs", "import": "./dist/utils/index.js" - } + }, + "./locales/*": "./dist/locales/*" }, "bin": { "barrel": "scripts/barrel.js" }, "scripts": { "clean": "rm -rf dist node_modules", - "build-intl": "build-intl --force -i ./locales -o ./src/i18n/translations.ts -n hightideTranslation", + "build-intl": "build-intl -c intl.config.json", "init": "pnpm run build-intl", - "build": "tsup", + "build": "tsup && cp -R locales dist/locales", "dev": "tsup --watch", "lint": "eslint .", "test": "jest" }, "dependencies": { - "@helpwave/internationalization": "0.4.0" + "@helpwave/internationalization": "0.5.0", + "zustand": "5.0.15" }, "peerDependencies": { "react": "^19.0.0" @@ -100,4 +112,4 @@ "bugs": { "url": "https://github.com/helpwave/hightide/issues" } -} +} \ No newline at end of file diff --git a/packages/hightide-utils/src/context/hightide-config.ts b/packages/hightide-utils/src/context/hightide-config.ts new file mode 100644 index 00000000..7787a513 --- /dev/null +++ b/packages/hightide-utils/src/context/hightide-config.ts @@ -0,0 +1,7 @@ +import type { LocalizationConfig } from './localization/LocalizationContext' +import type { ThemeConfig } from './theme/useCreateThemeConfig' + +export type HightideConfig = { + theme: ThemeConfig, + localization: LocalizationConfig, +} diff --git a/packages/hightide-utils/src/context/index.ts b/packages/hightide-utils/src/context/index.ts index 1c66e4c1..6f535dcc 100644 --- a/packages/hightide-utils/src/context/index.ts +++ b/packages/hightide-utils/src/context/index.ts @@ -1,4 +1,5 @@ export * from './debug' +export * from './hightide-config' export * from './localization' export * from './theme' export * from './translation' diff --git a/packages/hightide-utils/src/context/localization/HightideSupportedLocaleInformation.ts b/packages/hightide-utils/src/context/localization/HightideSupportedLocaleInformation.ts new file mode 100644 index 00000000..35a83520 --- /dev/null +++ b/packages/hightide-utils/src/context/localization/HightideSupportedLocaleInformation.ts @@ -0,0 +1,76 @@ +import type { LocaleInformation } from './LocalizationContext' + +export const HightideSupportedLocaleInformation: Record = { + 'de-DE': { + localeName: 'Deutsch', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, + 'en-US': { + localeName: 'English (US)', + defaultIs24HourFormat: false, + defaultStartingWeekday: 'sunday', + defaultCalendarType: 'Gregorian', + }, + 'pt-BR': { + localeName: 'Português (Brasil)', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'sunday', + defaultCalendarType: 'Gregorian', + }, + 'es-ES': { + localeName: 'Español', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, + 'fr-FR': { + localeName: 'Français', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, + 'nl-NL': { + localeName: 'Nederlands', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, + 'it-IT': { + localeName: 'Italiano', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, + 'pl-PL': { + localeName: 'Polski', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, + 'tr-TR': { + localeName: 'Türkçe', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, + 'ro-RO': { + localeName: 'Română', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, + 'el-GR': { + localeName: 'Ελληνικά', + defaultIs24HourFormat: false, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, + 'ru-RU': { + localeName: 'Русский', + defaultIs24HourFormat: true, + defaultStartingWeekday: 'monday', + defaultCalendarType: 'Gregorian', + }, +} diff --git a/packages/hightide-utils/src/context/localization/LocalizationContext.ts b/packages/hightide-utils/src/context/localization/LocalizationContext.ts index 563005cb..ca7a9d5f 100644 --- a/packages/hightide-utils/src/context/localization/LocalizationContext.ts +++ b/packages/hightide-utils/src/context/localization/LocalizationContext.ts @@ -8,8 +8,7 @@ export type TimeZone = string export type { Weekday, CalendarType } export type LocaleInformation = { - localName: string, - defaultTimeZone?: TimeZone, + localeName: string, defaultIs24HourFormat?: boolean, defaultStartingWeekday?: Weekday, defaultCalendarType?: CalendarType, @@ -17,6 +16,11 @@ export type LocaleInformation = { export type SupportedLocalesConfig = Record +export type LocalizationConfig = { + fallbackLocale: string, + supportedLocales: SupportedLocalesConfig, +} + export type LocalizationContextValue = { locale: Locale, setLocale: (locale: Locale) => void, diff --git a/packages/hightide-utils/src/context/localization/index.ts b/packages/hightide-utils/src/context/localization/index.ts index 4874cef8..7b12b857 100644 --- a/packages/hightide-utils/src/context/localization/index.ts +++ b/packages/hightide-utils/src/context/localization/index.ts @@ -1,4 +1,5 @@ export * from './LocalizationContext' +export * from './HightideSupportedLocaleInformation' export * from './LocalizationProvider' export * from './useCreateLocalizationContext' export * from './useDateTimeFormat' diff --git a/packages/hightide-utils/src/context/localization/useCreateLocalizationContext.ts b/packages/hightide-utils/src/context/localization/useCreateLocalizationContext.ts index 38d5e0fa..0d3c93e1 100644 --- a/packages/hightide-utils/src/context/localization/useCreateLocalizationContext.ts +++ b/packages/hightide-utils/src/context/localization/useCreateLocalizationContext.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo } from 'react' -import { useEventCallbackStabilizer } from '../../hooks/useEventCallbackStabelizer' +import { useStableEvent } from '../../hooks/useStableEvent' import { useSimpleStoreSyncedValue, type SimpleValueStore } from '../../hooks/useSimpleStoreSyncedValue' import type { LocalizationContextValue, SupportedLocalesConfig } from './LocalizationContext' import { StringUnionUtils } from '../../utils/stringUnion' @@ -146,8 +146,8 @@ export const useCreateLocalizationContext = ({ const localeDefaults = supportedLocales[resolvedLocale] const resolvedTimeZone = useMemo(() => { - return timeZone ?? storedTimeZone ?? localeDefaults?.defaultTimeZone ?? fallbackTimeZone - }, [timeZone, storedTimeZone, localeDefaults?.defaultTimeZone, fallbackTimeZone]) + return timeZone ?? storedTimeZone ?? fallbackTimeZone + }, [timeZone, storedTimeZone, fallbackTimeZone]) useEffect(() => { if (timeZone === undefined) return @@ -194,31 +194,31 @@ export const useCreateLocalizationContext = ({ setStoredCalendarType(calendarType) }, [calendarType, setStoredCalendarType]) - const onChangeRef = useEventCallbackStabilizer(onChangedLocale) + const onChangeRef = useStableEvent(onChangedLocale) useEffect(() => { onChangeRef?.(resolvedLocale) }, [resolvedLocale, onChangeRef]) - const onChangeTimeZoneRef = useEventCallbackStabilizer(onChangedTimeZone) + const onChangeTimeZoneRef = useStableEvent(onChangedTimeZone) useEffect(() => { onChangeTimeZoneRef?.(resolvedTimeZone) }, [resolvedTimeZone, onChangeTimeZoneRef]) - const onChangeIs24HourFormatRef = useEventCallbackStabilizer(onChangedIs24HourFormat) + const onChangeIs24HourFormatRef = useStableEvent(onChangedIs24HourFormat) useEffect(() => { onChangeIs24HourFormatRef?.(resolvedIs24HourFormat) }, [resolvedIs24HourFormat, onChangeIs24HourFormatRef]) - const onChangeStartingWeekdayRef = useEventCallbackStabilizer(onChangedStartingWeekday) + const onChangeStartingWeekdayRef = useStableEvent(onChangedStartingWeekday) useEffect(() => { onChangeStartingWeekdayRef?.(resolvedStartingWeekday) }, [resolvedStartingWeekday, onChangeStartingWeekdayRef]) - const onChangeCalendarTypeRef = useEventCallbackStabilizer(onChangedCalendarType) + const onChangeCalendarTypeRef = useStableEvent(onChangedCalendarType) useEffect(() => { onChangeCalendarTypeRef?.(resolvedCalendarType) diff --git a/packages/hightide-utils/src/context/theme/HightideSupportedThemeInformation.ts b/packages/hightide-utils/src/context/theme/HightideSupportedThemeInformation.ts new file mode 100644 index 00000000..d3cffe89 --- /dev/null +++ b/packages/hightide-utils/src/context/theme/HightideSupportedThemeInformation.ts @@ -0,0 +1,14 @@ +import type { ThemeInformation } from './useCreateThemeConfig' + +export const HightideSupportedThemeInformation = { + light: { + name: 'Light', + isDark: false, + translationKey: 'themeNameLight', + }, + dark: { + name: 'Dark', + isDark: true, + translationKey: 'themeNameDark', + }, +} as const satisfies Record> diff --git a/packages/hightide-utils/src/context/theme/index.ts b/packages/hightide-utils/src/context/theme/index.ts index 8f46672a..f2c4ed54 100644 --- a/packages/hightide-utils/src/context/theme/index.ts +++ b/packages/hightide-utils/src/context/theme/index.ts @@ -1 +1,2 @@ +export * from './HightideSupportedThemeInformation' export * from './useCreateThemeConfig' diff --git a/packages/hightide-utils/src/context/theme/useCreateThemeConfig.ts b/packages/hightide-utils/src/context/theme/useCreateThemeConfig.ts index 5da8c2a7..02661cc8 100644 --- a/packages/hightide-utils/src/context/theme/useCreateThemeConfig.ts +++ b/packages/hightide-utils/src/context/theme/useCreateThemeConfig.ts @@ -1,19 +1,46 @@ import { useCallback, useEffect, useMemo } from 'react' -import { useEventCallbackStabilizer } from '../../hooks/useEventCallbackStabelizer' +import { useStableEvent } from '../../hooks/useStableEvent' import type { SimpleValueStore } from '../../hooks/useSimpleStoreSyncedValue' import { useSimpleStoreSyncedValue } from '../../hooks/useSimpleStoreSyncedValue' -import type { Locale } from '../localization/LocalizationContext' export type SystemTheme = 'dark' | 'light' export type ThemeMode = SystemTheme | string export type ThemeInformation = { - nameTranslations: Record, - theme: T, + name: string, + isDark: boolean, + translationKey?: string, + nameTranslations?: Record, + theme?: T, +} + +const hasThemeInformationLabelSource = (themeInformation: ThemeInformation): boolean => { + if (themeInformation.translationKey) return true + return themeInformation.nameTranslations !== undefined && Object.keys(themeInformation.nameTranslations).length > 0 +} + +export const ThemeUtils = { + resolveThemeInformationLabel: ( + themeInformation: ThemeInformation, + locale: string, + translation: (key: string) => string + ): string => { + if (themeInformation.translationKey) { + return translation(themeInformation.translationKey) + } + return themeInformation.nameTranslations?.[locale] + ?? themeInformation.nameTranslations?.['en-US'] + ?? themeInformation.name + }, } -export type SupportedThemesConfig = Record> & Record> +export type SupportedThemesConfig = Record> & Record> + +export type ThemeConfig = { + initialTheme: string, + supportedThemes: SupportedThemesConfig, +} export type ThemeConfigValue = { themeMode: ThemeMode, @@ -44,6 +71,14 @@ export const useCreateThemeConfig = ({ throw new Error('useCreateThemeConfig: supportedThemes must contain at least one theme') } + useEffect(() => { + for (const [themeMode, themeInformation] of Object.entries(supportedThemes)) { + if (!hasThemeInformationLabelSource(themeInformation)) { + console.warn(`useCreateThemeConfig: theme "${themeMode}" has neither translationKey nor nameTranslations`) + } + } + }, [supportedThemes]) + const { value: storedTheme, setValue: setStoredTheme, @@ -80,7 +115,7 @@ export const useCreateThemeConfig = ({ return Object.keys(supportedThemes)[0] }, [fallbackTheme, preferredThemeMode, supportedThemes, systemTheme]) - const resolvedTheme: T = useMemo(() => supportedThemes[resolvedThemeMode].theme, [resolvedThemeMode, supportedThemes]) + const resolvedTheme: T = useMemo(() => supportedThemes[resolvedThemeMode].theme as T, [resolvedThemeMode, supportedThemes]) useEffect(() => { if (theme === undefined) return @@ -91,7 +126,7 @@ export const useCreateThemeConfig = ({ } }, [deleteStoredTheme, setStoredTheme, supportedThemes, theme]) - const onChangeRef = useEventCallbackStabilizer(onChangedTheme) + const onChangeRef = useStableEvent(onChangedTheme) useEffect(() => { onChangeRef?.(resolvedThemeMode) diff --git a/packages/hightide-utils/src/form/FormFieldWrapper.tsx b/packages/hightide-utils/src/form/FormFieldWrapper.tsx new file mode 100644 index 00000000..e12ee9ec --- /dev/null +++ b/packages/hightide-utils/src/form/FormFieldWrapper.tsx @@ -0,0 +1,81 @@ +import type { InputStateEvent } from '../interfaces/input' +import type { FormValues, ValidationRunnerState } from './FormStore' +import { useFormStore } from './FormStoreContext' +import { useFieldTouchedTrigger, type TouchedTrigger } from './useFieldTouchedTrigger' + +const emptyFieldErrors: string[] = [] + +export type FormFieldWrapperConfig< + T extends FormValues, + E extends InputStateEvent = InputStateEvent +> = { + isRequired?: boolean | ((values: T) => boolean), + isDisabled?: boolean | ((values: T) => boolean), + touchedTrigger?: TouchedTrigger, +} + +export type FormFieldWrapperBag = { + name: K, + + value: T[K], + errors: string[], + validationRunnerError: ValidationRunnerState | undefined, + + isTouched: boolean, + isRequired: boolean, + isDisabled: boolean, + + setValue: (value: T[K]) => void, + setTouched: (touched?: boolean) => void, + onStateEvent: (event: E) => void, +}; + +type FormFieldWrapperProps = { + name: K, + config?: FormFieldWrapperConfig, + children: (field: FormFieldWrapperBag) => React.ReactNode, +}; + +export function FormFieldWrapper< + T extends FormValues, + K extends keyof T, + E extends InputStateEvent = InputStateEvent +>({ + name, + config, + children, +}: FormFieldWrapperProps) { + const value = useFormStore()(state => state.values[name]) + const errors = useFormStore()(state => state.errors[name]) ?? emptyFieldErrors + const validationRunnerError = useFormStore()(state => state.validationRunnerError[name]) + const isTouched = useFormStore()(state => state.touchedValues[name] ?? false) + + const setValue = useFormStore()(state => state.setValue) + const setTouchedValue = useFormStore()(state => state.setTouchedValue) + const onStateEvent = useFieldTouchedTrigger(config?.touchedTrigger, value, () => { + setTouchedValue(name, true) + }) + + const isRequired = useFormStore()(state => + typeof config?.isRequired === 'function' + ? config.isRequired(state.values) + : config?.isRequired ?? false) + + const isDisabled = useFormStore()(state => + typeof config?.isDisabled === 'function' + ? config.isDisabled(state.values) + : config?.isDisabled ?? false) + + return children({ + name, + value, + errors, + validationRunnerError, + isTouched, + isRequired, + isDisabled, + setValue: value => setValue(name, value), + setTouched: touched => setTouchedValue(name, touched ?? true), + onStateEvent, + }) +} \ No newline at end of file diff --git a/packages/hightide-utils/src/form/FormStore.ts b/packages/hightide-utils/src/form/FormStore.ts new file mode 100644 index 00000000..d97a108d --- /dev/null +++ b/packages/hightide-utils/src/form/FormStore.ts @@ -0,0 +1,237 @@ +import { create, type StoreApi, type UseBoundStore } from 'zustand' + +export type FormValues = Record + +export type FormValidator = { + dependsOn: (keyof T)[] | 'all', + validatorFn: (values: T) => Partial>, +} + +export type CreateFormStoreOptions = { + initialValues: T, + initialTouchedValues?: Partial>, + validation?: FormValidator[], +} + +export type ValidationRunnerState = { + id: number, + isRunning: boolean, + errors: string[], +} + +export type FormFieldErrors = Partial> + +export type FormValidationRunnerError = Partial> + +export type FormState = { + values: T, + errors: FormFieldErrors, + validationRunnerError: FormValidationRunnerError, + touchedValues: Partial>, + + reset: () => void, + validate: () => number, + + setValue: ( + field: K, + value: T[K] + ) => void, + setValues: (values: Partial) => void, + + setValidationRunnerError: ( + field: K, + runner: ValidationRunnerState | undefined + ) => void, + + setTouchedValue: ( + field: K, + touched?: boolean + ) => void, + setTouchedValues: (touchedValues: Partial>) => void, +} + +export type FormStore = + UseBoundStore>> + +export const isValidationRunnerInvalid = (runner: ValidationRunnerState | undefined): boolean => { + if (!runner) return false + return runner.isRunning || runner.errors.length > 0 +} + +export const countFormErrors = ( + errors: FormFieldErrors, + validationRunnerError: FormValidationRunnerError +): number => { + const fields = new Set([ + ...(Object.keys(errors) as (keyof T)[]), + ...(Object.keys(validationRunnerError) as (keyof T)[]), + ]) + + let count = 0 + for (const field of fields) { + count += errors[field]?.length ?? 0 + const runner = validationRunnerError[field] + if (!runner) continue + if (runner.isRunning && runner.errors.length === 0) { + count += 1 + continue + } + count += runner.errors.length + } + + return count +} + +const appendFieldError = ( + errors: FormFieldErrors, + field: keyof T, + error: string | undefined +) => { + if (error === undefined) return + errors[field] = [...(errors[field] ?? []), error] +} + +const validatorDependsOnChange = ( + dependsOn: FormValidator['dependsOn'], + changedFields: (keyof T)[] +) => { + if (dependsOn === 'all') return true + return dependsOn.some(field => changedFields.includes(field)) +} + +const collectErrors = ( + validation: FormValidator[], + values: T +): FormFieldErrors => { + const errors: FormFieldErrors = {} + + for (const validator of validation) { + const result = validator.validatorFn(values) + for (const field of Object.keys(result) as (keyof T)[]) { + appendFieldError(errors, field, result[field]) + } + } + + return errors +} + +const revalidateChangedFields = ( + validation: FormValidator[], + errors: FormFieldErrors, + values: T, + changedFields: (keyof T)[] +): FormFieldErrors => { + const affected = validation.filter(validator => validatorDependsOnChange(validator.dependsOn, changedFields)) + const resetFields = new Set(changedFields) + const results = new Map, Partial>>() + + for (const validator of affected) { + const result = validator.validatorFn(values) + results.set(validator, result) + for (const field of Object.keys(result) as (keyof T)[]) { + resetFields.add(field) + } + } + + const nextErrors: FormFieldErrors = { ...errors } + for (const field of resetFields) { + nextErrors[field] = [] + } + + for (const validator of validation) { + const result = results.get(validator) ?? validator.validatorFn(values) + for (const field of Object.keys(result) as (keyof T)[]) { + if (!resetFields.has(field)) continue + appendFieldError(nextErrors, field, result[field]) + } + } + + for (const field of resetFields) { + if (!nextErrors[field]?.length) { + delete nextErrors[field] + } + } + + return nextErrors +} + +export function createFormStore({ + initialValues, + initialTouchedValues = {}, + validation = [], +}: CreateFormStoreOptions): FormStore { + return create>((set, get) => ({ + values: { ...initialValues }, + errors: collectErrors(validation, initialValues), + validationRunnerError: {}, + touchedValues: { ...initialTouchedValues }, + + setValues: (values) => { + set(state => { + const nextValues = { ...state.values, ...values } + return { + values: nextValues, + errors: revalidateChangedFields(validation, state.errors, nextValues, Object.keys(values) as (keyof T)[]), + } + }) + }, + + reset: () => { + set({ + values: { ...initialValues }, + errors: collectErrors(validation, initialValues), + validationRunnerError: {}, + touchedValues: { ...initialTouchedValues }, + }) + }, + + validate: () => { + const errors = collectErrors(validation, get().values) + set({ errors }) + return countFormErrors(errors, get().validationRunnerError) + }, + + setValue: (field, value) => { + set(state => { + const nextValues = { + ...state.values, + [field]: value, + } + return { + values: nextValues, + errors: revalidateChangedFields(validation, state.errors, nextValues, [field]), + } + }) + }, + + setValidationRunnerError: (field, runner) => { + set(state => { + const validationRunnerError = { ...state.validationRunnerError } + if (runner) { + validationRunnerError[field] = runner + } else { + delete validationRunnerError[field] + } + return { validationRunnerError } + }) + }, + + setTouchedValue: (field, touched = true) => { + set(state => ({ + touchedValues: { + ...state.touchedValues, + [field]: touched, + }, + })) + }, + + setTouchedValues: (touchedValues) => { + set(state => ({ + touchedValues: { + ...state.touchedValues, + ...touchedValues, + }, + })) + }, + })) +} diff --git a/packages/hightide-utils/src/form/FormStoreContext.tsx b/packages/hightide-utils/src/form/FormStoreContext.tsx new file mode 100644 index 00000000..8e949d3d --- /dev/null +++ b/packages/hightide-utils/src/form/FormStoreContext.tsx @@ -0,0 +1,32 @@ +import { createContext, useContext } from 'react' +import type { FormStore, FormValues } from './FormStore' + +export const FormStoreContext = createContext | null>(null) + +export function FormStoreProvider({ + store, + children, +}: { + store: FormStore, + children: React.ReactNode, +}) { + return ( + }> + {children} + + ) +} + +export function useFormStore< + T extends FormValues +>(): FormStore { + const store = useContext(FormStoreContext) + + if (!store) { + throw new Error( + 'FormField must be used inside a Form' + ) + } + + return store as FormStore +} \ No newline at end of file diff --git a/packages/hightide-utils/src/form/index.ts b/packages/hightide-utils/src/form/index.ts new file mode 100644 index 00000000..1072a342 --- /dev/null +++ b/packages/hightide-utils/src/form/index.ts @@ -0,0 +1,4 @@ +export * from './FormFieldWrapper' +export * from './useFieldTouchedTrigger' +export * from './FormStore' +export * from './FormStoreContext' diff --git a/packages/hightide-utils/src/form/useFieldTouchedTrigger.ts b/packages/hightide-utils/src/form/useFieldTouchedTrigger.ts new file mode 100644 index 00000000..a03df949 --- /dev/null +++ b/packages/hightide-utils/src/form/useFieldTouchedTrigger.ts @@ -0,0 +1,48 @@ +import { useCallback, useEffect, useRef } from 'react' +import type { InputStateEvent } from '../interfaces/input' +import { useDebouncer } from '../hooks/useDebouncer' +import { useStableEvent } from '../hooks/useStableEvent' +import { useThrottle } from '../hooks/useThrottle' + +export type TouchedTrigger = { + event?: ((event: E) => boolean) | null, + debounce?: number | null, + throttle?: number | null, +} + +const defaultTouchedTriggerDebounce = 1500 + +export function useFieldTouchedTrigger( + touchedTrigger: TouchedTrigger | undefined, + value: unknown, + markTouched: () => void +): (event: E) => void { + const matchesEvent = touchedTrigger?.event === undefined ? null : touchedTrigger.event + const debounce = touchedTrigger?.debounce === undefined ? defaultTouchedTriggerDebounce : touchedTrigger.debounce + const throttle = touchedTrigger?.throttle === undefined ? null : touchedTrigger.throttle + + const withDebounce = useDebouncer(typeof debounce === 'number' ? debounce : 0) + const withThrottle = useThrottle(typeof throttle === 'number' ? throttle : 0) + const markTouchedStable = useStableEvent(markTouched) + const matchesEventStable = useStableEvent<(event: E) => boolean>(matchesEvent ?? (() => false)) + const previousValueRef = useRef(value) + + useEffect(() => { + if (Object.is(previousValueRef.current, value)) { + return + } + previousValueRef.current = value + if (typeof debounce === 'number') { + withDebounce(markTouchedStable, debounce) + } + if (typeof throttle === 'number') { + withThrottle(markTouchedStable, throttle) + } + }, [debounce, markTouchedStable, throttle, value, withDebounce, withThrottle]) + + return useCallback((event: E) => { + if (matchesEvent !== null && matchesEventStable(event)) { + markTouchedStable() + } + }, [markTouchedStable, matchesEvent, matchesEventStable]) +} diff --git a/packages/hightide-utils/src/hooks/index.ts b/packages/hightide-utils/src/hooks/index.ts index 271a1c2d..0dd46ff6 100644 --- a/packages/hightide-utils/src/hooks/index.ts +++ b/packages/hightide-utils/src/hooks/index.ts @@ -4,7 +4,7 @@ export * from './useChangingNumber' export * from './useControlledState' export * from './useDebouncer' export * from './useDelay' -export * from './useEventCallbackStabelizer' +export * from './useEditCompletable' export * from './useListNavigation' export * from './useLogOnce' export * from './useLogUnstableDependencies' @@ -16,6 +16,8 @@ export * from './useRerender' export * from './useSearch' export * from './useSimpleStoreSyncedValue' export * from './useSingleSelection' +export * from './useStableEvent' +export * from './state-machine' export * from './useStepperHold' export * from './useThrottle' export * from './useTypeAheadSearch' diff --git a/packages/hightide-utils/src/hooks/state-machine/StateStore.ts b/packages/hightide-utils/src/hooks/state-machine/StateStore.ts new file mode 100644 index 00000000..20bb449f --- /dev/null +++ b/packages/hightide-utils/src/hooks/state-machine/StateStore.ts @@ -0,0 +1,26 @@ +import type { StateStore } from '../../interfaces/state-store' + +export type { StateStore } + +export function createStateStore(initial: T): StateStore { + let current = initial + const listeners = new Set<() => void>() + + return { + getSnapshot: () => current, + set: (value: T) => { + if (Object.is(current, value)) return + current = value + const pending = [...listeners] + for (const listener of pending) { + listener() + } + }, + subscribe: (listener: () => void) => { + listeners.add(listener) + return () => { + listeners.delete(listener) + } + }, + } +} diff --git a/packages/hightide-utils/src/hooks/state-machine/index.ts b/packages/hightide-utils/src/hooks/state-machine/index.ts new file mode 100644 index 00000000..e642bc95 --- /dev/null +++ b/packages/hightide-utils/src/hooks/state-machine/index.ts @@ -0,0 +1,14 @@ +export { createStateStore, type StateStore } from './StateStore' +export { useStateMachine, type StateMachine, type StateMachineProps } from './useStateMachine' +export { useStateStore } from './useStateStore' +export { + useControllableStateStore, + type UseControllableStateStoreProps, + type UseControllableStateStoreResult, +} from './useControllableStateStore' +export { + useStateStoreBindings, + type StateStoreBinding, + type StateStoreBindings, + type UseStateStoreBindingsProps, +} from './useStateStoreBindings' diff --git a/packages/hightide-utils/src/hooks/state-machine/useControllableStateStore.ts b/packages/hightide-utils/src/hooks/state-machine/useControllableStateStore.ts new file mode 100644 index 00000000..ca1bdb4f --- /dev/null +++ b/packages/hightide-utils/src/hooks/state-machine/useControllableStateStore.ts @@ -0,0 +1,59 @@ +import { useCallback, useEffect, useRef } from 'react' +import type { StateStore } from '../../interfaces/state-store' +import { useStableEvent } from '../useStableEvent' +import { useStateStore } from './useStateStore' + +export type UseControllableStateStoreProps = { + onStateChange?: (state: S) => void, +} & ( + | { + state: S, + initialState?: S, + } + | { + state?: undefined, + initialState: S, + } + ) + +export type UseControllableStateStoreResult = { + store: StateStore, + onStateChange: (state: S) => void, +} + +export function useControllableStateStore({ + state, + initialState, + onStateChange, +}: UseControllableStateStoreProps): UseControllableStateStoreResult { + const store = useStateStore(() => state !== undefined ? state : initialState as S) + const hadStateRef = useRef(state !== undefined) + const onStateChangeStable = useStableEvent(onStateChange) + + useEffect(() => { + const hasState = state !== undefined + if (hasState !== hadStateRef.current) { + console.error('useControllableStateStore: state changed from a defined value to undefined. Either consistently provide a state or only provide an initial state.') + return + } + if (state) { + if (store.getSnapshot() !== state) { + store.set(state) + } + } + }, [store, state]) + + const handleStateChange = useCallback((next: S) => { + const last = store.getSnapshot() + if (!hadStateRef.current) { + store.set(next) + } + if (last !== next) + onStateChangeStable(next) + }, [onStateChangeStable, store]) + + return { + store, + onStateChange: handleStateChange, + } +} diff --git a/packages/hightide-utils/src/hooks/state-machine/useStateMachine.ts b/packages/hightide-utils/src/hooks/state-machine/useStateMachine.ts new file mode 100644 index 00000000..11b91e92 --- /dev/null +++ b/packages/hightide-utils/src/hooks/state-machine/useStateMachine.ts @@ -0,0 +1,40 @@ +import { useCallback, useMemo, useSyncExternalStore } from 'react' +import type { StateStore } from '../../interfaces/state-store' +import { useStableEvent } from '../useStableEvent' + +export type StateMachineProps = { + state: StateStore, + transition: (state: S, event: E) => S, + onStateChange?: (state: S) => void, + onStateEvent?: (event: E) => void, +} + +export type StateMachine = { + state: S, + dispatch: (event: E) => void, +} + +export function useStateMachine({ + state, + transition, + onStateChange, + onStateEvent, +}: StateMachineProps): StateMachine { + const transitionStable = useStableEvent(transition) + const onStateChangeStable = useStableEvent(onStateChange) + const onStateEventStable = useStableEvent(onStateEvent) + const storeState = useSyncExternalStore(state.subscribe, state.getSnapshot, state.getSnapshot) + const getSnapshot = state.getSnapshot + + const dispatch = useCallback((event: E) => { + const current = getSnapshot() + const next = transitionStable(current, event) + onStateChangeStable(next) + onStateEventStable(event) + }, [getSnapshot, onStateChangeStable, onStateEventStable, transitionStable]) + + return useMemo(() => ({ + state: storeState, + dispatch, + }), [dispatch, storeState]) +} diff --git a/packages/hightide-utils/src/hooks/state-machine/useStateStore.ts b/packages/hightide-utils/src/hooks/state-machine/useStateStore.ts new file mode 100644 index 00000000..1059ed98 --- /dev/null +++ b/packages/hightide-utils/src/hooks/state-machine/useStateStore.ts @@ -0,0 +1,17 @@ +import { useRef } from 'react' +import { createStateStore, type StateStore } from './StateStore' + +function resolveInitialValue(initial: T | (() => T)): T { + if (typeof initial === 'function') { + return (initial as () => T)() + } + return initial +} + +export function useStateStore(initial: T | (() => T)): StateStore { + const storeRef = useRef | null>(null) + if (storeRef.current === null) { + storeRef.current = createStateStore(resolveInitialValue(initial)) + } + return storeRef.current +} diff --git a/packages/hightide-utils/src/hooks/state-machine/useStateStoreBindings.ts b/packages/hightide-utils/src/hooks/state-machine/useStateStoreBindings.ts new file mode 100644 index 00000000..41d5c79a --- /dev/null +++ b/packages/hightide-utils/src/hooks/state-machine/useStateStoreBindings.ts @@ -0,0 +1,119 @@ +import { useCallback, useEffect, useRef } from 'react' +import type { StateStore } from '../../interfaces/state-store' + +export type StateStoreBinding = { + value?: S[K], + onValueChange?: (value: S[K]) => boolean | undefined | void, +} + +export type StateStoreBindings = { + [K in keyof S]?: StateStoreBinding +} + +export type UseStateStoreBindingsProps = { + store: StateStore, + bindings: StateStoreBindings, +} + +type BindingMode = 'controlled' | 'uncontrolled' + +function bindingKeys(bindings: StateStoreBindings): (keyof S)[] { + return Object.keys(bindings) as (keyof S)[] +} + +export function useStateStoreBindings({ + store, + bindings, +}: UseStateStoreBindingsProps): (next: S) => void { + const bindingsRef = useRef(bindings) + bindingsRef.current = bindings + const modesRef = useRef> | null>(null) + const seenControlledRef = useRef>>({}) + + if (modesRef.current === null) { + const modes: Partial> = {} + for (const key of bindingKeys(bindings)) { + const isControlled = bindings[key]?.value !== undefined + modes[key] = isControlled ? 'controlled' : 'uncontrolled' + seenControlledRef.current[key] = isControlled + } + modesRef.current = modes + } + + for (const key of bindingKeys(bindings)) { + const isControlled = bindings[key]?.value !== undefined + const seen = seenControlledRef.current[key] + if (seen === undefined || seen === isControlled) { + seenControlledRef.current[key] = isControlled + continue + } + seenControlledRef.current[key] = isControlled + const keyName = String(key) + if (seen && !isControlled) { + console.error( + `useStateStoreBindings: value for "${keyName}" changed from a defined value to undefined. A binding stays controlled or uncontrolled.` + ) + } else { + console.error( + `useStateStoreBindings: value for "${keyName}" changed from undefined to a defined value. A binding stays controlled or uncontrolled.` + ) + } + } + + useEffect(() => { + const modes = modesRef.current + if (!modes) { + return + } + const current = store.getSnapshot() + let committed = current + let changed = false + for (const key of bindingKeys(bindings)) { + if (modes[key] !== 'controlled') { + continue + } + const value = bindings[key]?.value + if (value === undefined || Object.is(current[key], value)) { + continue + } + if (!changed) { + committed = { ...current } + changed = true + } + committed = { ...committed, [key]: value } + } + if (changed) { + store.set(committed) + } + }) + + return useCallback((next: S) => { + const current = store.getSnapshot() + const currentBindings = bindingsRef.current + let committed = current + let changed = false + const keys = new Set([ + ...bindingKeys(currentBindings), + ...(Object.keys(next) as (keyof S)[]), + ]) + for (const key of keys) { + const binding = currentBindings[key] + const proposed = next[key] + if (Object.is(proposed, current[key])) { + continue + } + const accepted = binding?.onValueChange?.(proposed) + if (accepted === false) { + continue + } + if (!changed) { + committed = { ...current } + changed = true + } + committed = { ...committed, [key]: proposed } + } + if (changed) { + store.set(committed) + } + }, [store]) +} diff --git a/packages/hightide-utils/src/hooks/useControlledState.ts b/packages/hightide-utils/src/hooks/useControlledState.ts index c4e8164f..561fc410 100644 --- a/packages/hightide-utils/src/hooks/useControlledState.ts +++ b/packages/hightide-utils/src/hooks/useControlledState.ts @@ -1,7 +1,7 @@ import type React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { resolveSetState } from '../utils/setStateUtils' -import { useEventCallbackStabilizer } from './useEventCallbackStabelizer' +import { useStableEvent } from './useStableEvent' import { useLogOnce } from './useLogOnce' export interface ControlledStateProps { @@ -32,7 +32,7 @@ export const useControlledState = ({ const [internalValue, setInternalValue] = useState(() => defaultValue) const [isControlled] = useState(isEnforcingControlled || controlledValue !== undefined) - const onValueChangeStable = useEventCallbackStabilizer(onValueChange) + const onValueChangeStable = useStableEvent(onValueChange) useLogOnce( 'useControlledState: Attempted to change from controlled to uncontrolled or vice versa.' diff --git a/packages/hightide-utils/src/hooks/useEditCompletable.ts b/packages/hightide-utils/src/hooks/useEditCompletable.ts new file mode 100644 index 00000000..380cec22 --- /dev/null +++ b/packages/hightide-utils/src/hooks/useEditCompletable.ts @@ -0,0 +1,64 @@ +import type { KeyboardEvent } from 'react' +import { useCallback, useRef } from 'react' +import { useDelay } from './useDelay' +import { useStableEvent } from './useStableEvent' + +export type UseEditCompletableProps = { + value: T, + onEditComplete?: (value: T) => void, + delay?: number, + isTimerEnabled?: boolean, + completeKey?: string, +} + +export type UseEditCompletableResult = { + setTimer: () => void, + completeOnKey: (event: KeyboardEvent) => boolean, + completeNow: () => void, +} + +export function useEditCompletable({ + value, + onEditComplete, + delay = 2500, + isTimerEnabled = true, + completeKey = 'Enter', +}: UseEditCompletableProps): UseEditCompletableResult { + const valueRef = useRef(value) + valueRef.current = value + const onEditCompleteStable = useStableEvent(onEditComplete) + + const completeEdit = useCallback(() => { + onEditCompleteStable(valueRef.current) + }, [onEditCompleteStable]) + + const { restartTimer, clearTimer } = useDelay({ + delay, + disabled: !isTimerEnabled, + }) + + const setTimer = useCallback(() => { + restartTimer(completeEdit) + }, [completeEdit, restartTimer]) + + const completeOnKey = useCallback((event: KeyboardEvent) => { + if (event.key !== completeKey || event.shiftKey) { + return false + } + event.preventDefault() + clearTimer() + completeEdit() + return true + }, [clearTimer, completeEdit, completeKey]) + + const completeNow = useCallback(() => { + clearTimer() + completeEdit() + }, [clearTimer, completeEdit]) + + return { + setTimer, + completeOnKey, + completeNow, + } +} diff --git a/packages/hightide-utils/src/hooks/useSearch.ts b/packages/hightide-utils/src/hooks/useSearch.ts index 5f31595f..6c4f66e2 100644 --- a/packages/hightide-utils/src/hooks/useSearch.ts +++ b/packages/hightide-utils/src/hooks/useSearch.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { MultiSearchWithMapping } from '../utils/simpleSearch' -import { useEventCallbackStabilizer } from './useEventCallbackStabelizer' +import { useStableEvent } from './useStableEvent' export interface UseSearchOptions { items: ReadonlyArray, @@ -22,7 +22,7 @@ export function useSearch({ toTags, }: UseSearchOptions): UseSearchReturn { const toTagsResolved = toTags ?? defaultToTags - const toTagsStable = useEventCallbackStabilizer(toTagsResolved) + const toTagsStable = useStableEvent(toTagsResolved) const searchResult = useMemo(() => { const q = searchQuery.trim().toLowerCase() diff --git a/packages/hightide-utils/src/hooks/useSimpleStoreSyncedValue.ts b/packages/hightide-utils/src/hooks/useSimpleStoreSyncedValue.ts index 9018050f..fe330223 100644 --- a/packages/hightide-utils/src/hooks/useSimpleStoreSyncedValue.ts +++ b/packages/hightide-utils/src/hooks/useSimpleStoreSyncedValue.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react' import { useControlledState } from './useControlledState' -import { useEventCallbackStabilizer } from './useEventCallbackStabelizer' +import { useStableEvent } from './useStableEvent' export interface KeyValueStore> { getValue(key: K): TSchema[K], @@ -81,7 +81,7 @@ export function useSimpleStoreSyncedValue({ store.isInitialized, ]) - const encodeStable = useEventCallbackStabilizer(encode) + const encodeStable = useStableEvent(encode) useEffect(() => { if (!store.isInitialized || !hasRead) return diff --git a/packages/hightide-utils/src/hooks/useEventCallbackStabelizer.ts b/packages/hightide-utils/src/hooks/useStableEvent.ts similarity index 91% rename from packages/hightide-utils/src/hooks/useEventCallbackStabelizer.ts rename to packages/hightide-utils/src/hooks/useStableEvent.ts index 975f4d09..edf391b8 100644 --- a/packages/hightide-utils/src/hooks/useEventCallbackStabelizer.ts +++ b/packages/hightide-utils/src/hooks/useStableEvent.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef } from 'react' -export function useEventCallbackStabilizer< +export function useStableEvent< // eslint-disable-next-line @typescript-eslint/no-explicit-any T extends (...args: any[]) => any >( diff --git a/packages/hightide-utils/src/hooks/useTypeAheadSearch.ts b/packages/hightide-utils/src/hooks/useTypeAheadSearch.ts index f951bc77..fdf1b6b2 100644 --- a/packages/hightide-utils/src/hooks/useTypeAheadSearch.ts +++ b/packages/hightide-utils/src/hooks/useTypeAheadSearch.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef } from 'react' -import { useEventCallbackStabilizer } from './useEventCallbackStabelizer' +import { useStableEvent } from './useStableEvent' export interface UseTypeAheadSearchOptions { options: ReadonlyArray, @@ -27,8 +27,8 @@ export function useTypeAheadSearch({ const timeoutRef = useRef | null>(null) const toString = toStringProp ?? defaultToString - const toStringStable = useEventCallbackStabilizer(toString) - const onResultChangeStable = useEventCallbackStabilizer(onResultChange) + const toStringStable = useStableEvent(toString) + const onResultChangeStable = useStableEvent(onResultChange) const reset = useCallback(() => { if (timeoutRef.current) { diff --git a/packages/hightide-utils/src/interfaces/index.ts b/packages/hightide-utils/src/interfaces/index.ts new file mode 100644 index 00000000..d6ce747c --- /dev/null +++ b/packages/hightide-utils/src/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './input' +export * from './state-manager' +export * from './state-store' diff --git a/packages/hightide-utils/src/interfaces/input.ts b/packages/hightide-utils/src/interfaces/input.ts new file mode 100644 index 00000000..0e1d1f95 --- /dev/null +++ b/packages/hightide-utils/src/interfaces/input.ts @@ -0,0 +1,26 @@ +import type { StateManagerProps } from './state-manager' + +export interface InputState { value: unknown } +export interface InputStateEvent { type: string } +export interface InputConfig { + isInvalid: boolean, + isDisabled: boolean, + isReadOnly: boolean, + isRequired: boolean, +} +export interface InputContextValue { + state: T, + dispatch: (event: E) => void, + config: C, +} + +export interface ControllableInputProps extends Partial { + value?: T, + initialValue?: T, + onValueChange?: (value: T) => void, + onStateEvent?: (event: E) => void, +} + +export interface InputStateMangerProps extends + Partial, + StateManagerProps { } diff --git a/packages/hightide-utils/src/interfaces/state-manager.ts b/packages/hightide-utils/src/interfaces/state-manager.ts new file mode 100644 index 00000000..f6a75675 --- /dev/null +++ b/packages/hightide-utils/src/interfaces/state-manager.ts @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' +import type { StateStore } from './state-store' + +export type StateManagerProps = { + state: StateStore, + onStateChange?: (state: T) => void, + onStateEvent?: (event: E) => void, + children: ReactNode, +} diff --git a/packages/hightide-utils/src/interfaces/state-store.ts b/packages/hightide-utils/src/interfaces/state-store.ts new file mode 100644 index 00000000..c95fe8c0 --- /dev/null +++ b/packages/hightide-utils/src/interfaces/state-store.ts @@ -0,0 +1,5 @@ +export type StateStore = { + getSnapshot: () => T, + set: (value: T) => void, + subscribe: (listener: () => void) => () => void, +} diff --git a/packages/hightide-utils/src/utils/README.md b/packages/hightide-utils/src/utils/README.md index f895c00d..54b924ee 100644 --- a/packages/hightide-utils/src/utils/README.md +++ b/packages/hightide-utils/src/utils/README.md @@ -12,7 +12,6 @@ See [conventions](../../conventions/README.md) for how Utils objects, deprecatio | [curve.ts](./curve.ts) | `CurveBuilderUtil` | Easing and rate curves including cubic bezier and exponential builders | | [date.ts](./date.ts) | `DateUtils` | Date/time parsing, formatting, time zones, calendars, and duration arithmetic | | [duration.ts](./duration.ts) | `DurationUtils` | Immutable duration value object with date arithmetic | -| [emailValidation.ts](./emailValidation.ts) | `EmailValidationUtils` | Email address format validation | | [loopingArray.ts](./loopingArray.ts) | `LoopingArrayUtils` | Circular array position, distance, and direction calculations | | [match.ts](./match.ts) | `MatchUtils` | Key lookup against a record map | | [math.ts](./math.ts) | `MathUtil` | Clamping, step ranges, and looping numeric bounds | diff --git a/packages/hightide-utils/src/utils/componentState.ts b/packages/hightide-utils/src/utils/componentState.ts new file mode 100644 index 00000000..d00ef60f --- /dev/null +++ b/packages/hightide-utils/src/utils/componentState.ts @@ -0,0 +1,48 @@ +import type { EnumUtilsType } from './enum' + +const loadingStateValues = ['idle', 'loading', 'blocked', 'processing'] as const +export type LoadingState = (typeof loadingStateValues)[number] +const allowedLoadingStateValues: ReadonlySet = new Set(loadingStateValues) +function isLoadingStateValue(value: unknown): value is LoadingState { + if (typeof value !== 'string') return false + return allowedLoadingStateValues.has(value) +} +export const LoadingStateUtils: EnumUtilsType = { + values: loadingStateValues, + set: allowedLoadingStateValues, + isValue: isLoadingStateValue, +} + +const interactiveStateValues = ['hover', 'pressed', 'focus', 'focus-visible', 'disabled'] as const +export type InteractiveState = (typeof interactiveStateValues)[number] +const allowedInteractiveStateValues: ReadonlySet = new Set(interactiveStateValues) +function isInteractiveStateValue(value: unknown): value is InteractiveState { + if (typeof value !== 'string') return false + return allowedInteractiveStateValues.has(value) +} +export const InteractiveStateUtils: EnumUtilsType = { + values: interactiveStateValues, + set: allowedInteractiveStateValues, + isValue: isInteractiveStateValue, +} + +const dataInputStateValues = [ + 'readonly', + 'invalid', + 'hover', + 'pressed', + 'focus', + 'focus-visible', + 'disabled', +] as const +export type DataInputState = (typeof dataInputStateValues)[number] +const allowedDataInputStateValues: ReadonlySet = new Set(dataInputStateValues) +function isDataInputStateValue(value: unknown): value is DataInputState { + if (typeof value !== 'string') return false + return allowedDataInputStateValues.has(value) +} +export const DataInputStateUtils: EnumUtilsType = { + values: dataInputStateValues, + set: allowedDataInputStateValues, + isValue: isDataInputStateValue, +} diff --git a/packages/hightide-utils/src/utils/date.ts b/packages/hightide-utils/src/utils/date.ts index f4e7bd09..75b5d261 100644 --- a/packages/hightide-utils/src/utils/date.ts +++ b/packages/hightide-utils/src/utils/date.ts @@ -1,4 +1,4 @@ -import { equalSizeGroups } from './array' +import { equalSizeGroups, range } from './array' import type { DurationJSON } from './duration' const DateTimeFormat = ['date', 'time', 'dateTime'] as const @@ -134,6 +134,41 @@ const daysInMonth = (year: number, monthIndex: number): number => { return new Date(year, monthIndex + 1, 0).getDate() } +const monthsForYear = (year: number, minTimestamp?: number, maxTimestamp?: number): number[] => { + return range(12).filter((month) => { + const monthStart = new Date(year, month, 1).getTime() + const monthEnd = new Date(year, month + 1, 0).getTime() + const isAfterStart = minTimestamp === undefined || monthEnd >= minTimestamp + const isBeforeEnd = maxTimestamp === undefined || monthStart <= maxTimestamp + return isAfterStart && isBeforeEnd + }) +} + +const withYearMonth = (date: Date, year: number, month: number): Date => { + const next = new Date(date) + const lastDay = daysInMonth(year, month) + next.setFullYear(year, month, Math.min(date.getDate(), lastDay)) + return next +} + +const withDateParts = (date: Date, year: number, month: number, day: number): Date => { + const next = new Date(date) + const lastDay = daysInMonth(year, month) + next.setFullYear(year, month, Math.min(day, lastDay)) + return next +} + +const monthBounds = (start: Date, end: Date): { minTimestamp: number, maxTimestamp: number } => { + return { + minTimestamp: new Date(start.getFullYear(), start.getMonth(), 1).getTime(), + maxTimestamp: new Date(end.getFullYear(), end.getMonth() + 1, 0).getTime(), + } +} + +const yearsBetween = (start: Date, end: Date): number[] => { + return range([start.getFullYear(), end.getFullYear()], { exclusiveEnd: false }) +} + const sameTime = (a: Date, b: Date, compareSeconds: boolean = false, compareMilliseconds: boolean = false): boolean => { if (a.getHours() !== b.getHours() || a.getMinutes() !== b.getMinutes()) { @@ -393,6 +428,11 @@ export const DateUtils = { equalDate, isLastMillisecondOfDay, daysInMonth, + monthsForYear, + withYearMonth, + withDateParts, + monthBounds, + yearsBetween, sameTime, withTime, zonedParts, diff --git a/packages/hightide-utils/src/utils/emailValidation.ts b/packages/hightide-utils/src/utils/emailValidation.ts deleted file mode 100644 index a7d9dd16..00000000 --- a/packages/hightide-utils/src/utils/emailValidation.ts +++ /dev/null @@ -1,10 +0,0 @@ -const validateEmail = (email: string): boolean => { - return /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email) -} - -export const EmailValidationUtils = { - validateEmail, -} - -/** @deprecated Use EmailValidationUtils.validateEmail instead. */ -export { validateEmail } diff --git a/packages/hightide-utils/src/utils/form-validation/FormValidationErrorType.ts b/packages/hightide-utils/src/utils/form-validation/FormValidationErrorType.ts new file mode 100644 index 00000000..568a4a50 --- /dev/null +++ b/packages/hightide-utils/src/utils/form-validation/FormValidationErrorType.ts @@ -0,0 +1,57 @@ +const stringFormValidationErrorTypeValues = [ + 'notEmpty', + 'invalidEmail', + 'tooShort', + 'tooLong', + 'outOfRangeString', +] as const + +const numberFormValidationErrorTypeValues = [ + 'notEmpty', + 'outOfRangeNumber', +] as const + +const selectionFormValidationErrorTypeValues = [ + 'notEmpty', + 'tooFewSelectionItems', + 'tooManySelectionItems', + 'outOfRangeSelectionItems', +] as const + +const formValidationErrorTypeValues = [ + 'notEmpty', + 'invalidEmail', + 'tooShort', + 'tooLong', + 'outOfRangeString', + 'outOfRangeNumber', + 'tooFewSelectionItems', + 'tooManySelectionItems', + 'outOfRangeSelectionItems', +] as const + +export type StringFormValidationErrorType = (typeof stringFormValidationErrorTypeValues)[number] + +export type NumberFormValidationErrorType = (typeof numberFormValidationErrorTypeValues)[number] + +export type SelectionFormValidationErrorType = (typeof selectionFormValidationErrorTypeValues)[number] + +export type FormValidationErrorType = (typeof formValidationErrorTypeValues)[number] + +export type FormValidationResult = FormValidationErrorType | undefined + +const allowedFormValidationErrorTypeValues: ReadonlySet = new Set(formValidationErrorTypeValues) + +function isFormValidationErrorTypeValue(value: unknown): value is FormValidationErrorType { + if (typeof value !== 'string') return false + return allowedFormValidationErrorTypeValues.has(value) +} + +export const FormValidationErrorTypeUtils = { + array: formValidationErrorTypeValues, + string: stringFormValidationErrorTypeValues, + number: numberFormValidationErrorTypeValues, + selection: selectionFormValidationErrorTypeValues, + set: allowedFormValidationErrorTypeValues, + typeCheck: isFormValidationErrorTypeValue, +} diff --git a/packages/hightide-utils/src/utils/form-validation/FormValidationUtils.ts b/packages/hightide-utils/src/utils/form-validation/FormValidationUtils.ts new file mode 100644 index 00000000..543aafb8 --- /dev/null +++ b/packages/hightide-utils/src/utils/form-validation/FormValidationUtils.ts @@ -0,0 +1,23 @@ +import { notEmpty as numberNotEmpty, range } from './number' +import { bounds, notEmpty as selectionNotEmpty } from './selection' +import { email, length, notEmpty as stringNotEmpty } from './string' +import { mapNumberTranslation, mapSelectionTranslation, mapStringTranslation } from './translation' + +export const FormValidationUtils = { + string: { + notEmpty: stringNotEmpty, + email, + length, + mapTranslation: mapStringTranslation, + }, + number: { + notEmpty: numberNotEmpty, + range, + mapTranslation: mapNumberTranslation, + }, + selection: { + notEmpty: selectionNotEmpty, + bounds, + mapTranslation: mapSelectionTranslation, + }, +} diff --git a/packages/hightide-utils/src/utils/form-validation/bounds.ts b/packages/hightide-utils/src/utils/form-validation/bounds.ts new file mode 100644 index 00000000..e28ca0f2 --- /dev/null +++ b/packages/hightide-utils/src/utils/form-validation/bounds.ts @@ -0,0 +1,21 @@ +export type FormValidationBounds = [number | undefined, number | undefined] + +export type BoundsValidationResult = 'lower' | 'upper' | 'range' | 'none' + +export const boundsValidation = (length: number | undefined, bounds: FormValidationBounds): BoundsValidationResult => { + const [min, max] = bounds + + if (min !== undefined && max !== undefined && (length === undefined || length < min || length > max)) { + return 'range' + } + + if (min !== undefined && (length === undefined || length < min)) { + return 'lower' + } + + if (max !== undefined && length !== undefined && length > max) { + return 'upper' + } + + return 'none' +} diff --git a/packages/hightide-utils/src/utils/form-validation/index.ts b/packages/hightide-utils/src/utils/form-validation/index.ts new file mode 100644 index 00000000..c50aa386 --- /dev/null +++ b/packages/hightide-utils/src/utils/form-validation/index.ts @@ -0,0 +1,11 @@ +export { FormValidationUtils } from './FormValidationUtils' +export { FormValidationErrorTypeUtils } from './FormValidationErrorType' +export type { FormValidationBounds } from './bounds' +export type { + FormValidationErrorType, + FormValidationResult, + NumberFormValidationErrorType, + SelectionFormValidationErrorType, + StringFormValidationErrorType, +} from './FormValidationErrorType' +export type { FormValidationTranslation, FormValidationTranslationInput, FormValidationTranslationParameters } from './translation' diff --git a/packages/hightide-utils/src/utils/form-validation/number.ts b/packages/hightide-utils/src/utils/form-validation/number.ts new file mode 100644 index 00000000..46aee666 --- /dev/null +++ b/packages/hightide-utils/src/utils/form-validation/number.ts @@ -0,0 +1,21 @@ +import { boundsValidation, type FormValidationBounds } from './bounds' +import type { FormValidationResult } from './FormValidationErrorType' + +export const notEmpty = (value: number | undefined | null): FormValidationResult => { + if (value === undefined || value === null || Number.isNaN(value)) { + return 'notEmpty' + } +} + +export const range = (value: number | undefined | null, bounds: FormValidationBounds): FormValidationResult => { + if (value === undefined || value === null || Number.isNaN(value)) { + return undefined + } + + const result = boundsValidation(value, bounds) + if (result === 'none') { + return undefined + } + + return 'outOfRangeNumber' +} diff --git a/packages/hightide-utils/src/utils/form-validation/selection.ts b/packages/hightide-utils/src/utils/form-validation/selection.ts new file mode 100644 index 00000000..4f1e8c02 --- /dev/null +++ b/packages/hightide-utils/src/utils/form-validation/selection.ts @@ -0,0 +1,18 @@ +import { boundsValidation, type FormValidationBounds } from './bounds' +import type { FormValidationResult } from './FormValidationErrorType' + +export const notEmpty = (value: readonly unknown[] | undefined): FormValidationResult => { + if (!value || value.length === 0) { + return 'notEmpty' + } +} + +export const bounds = (value: readonly unknown[] | undefined, limits: FormValidationBounds): FormValidationResult => { + const mapping: Record, FormValidationResult> = { + range: 'outOfRangeSelectionItems', + lower: 'tooFewSelectionItems', + upper: 'tooManySelectionItems', + none: undefined, + } + return mapping[boundsValidation(value?.length, limits)] +} diff --git a/packages/hightide-utils/src/utils/form-validation/string.ts b/packages/hightide-utils/src/utils/form-validation/string.ts new file mode 100644 index 00000000..31a40a6c --- /dev/null +++ b/packages/hightide-utils/src/utils/form-validation/string.ts @@ -0,0 +1,26 @@ +import { boundsValidation, type FormValidationBounds } from './bounds' +import type { FormValidationResult } from './FormValidationErrorType' + +const emailPattern = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i + +export const notEmpty = (value: string | undefined): FormValidationResult => { + if (!value) { + return 'notEmpty' + } +} + +export const email = (value: string | undefined): FormValidationResult => { + if (!value || !emailPattern.test(value)) { + return 'invalidEmail' + } +} + +export const length = (value: string | undefined, bounds: FormValidationBounds): FormValidationResult => { + const mapping: Record, FormValidationResult> = { + range: 'outOfRangeString', + lower: 'tooShort', + upper: 'tooLong', + none: undefined, + } + return mapping[boundsValidation(value?.length, bounds)] +} diff --git a/packages/hightide-utils/src/utils/form-validation/translation.ts b/packages/hightide-utils/src/utils/form-validation/translation.ts new file mode 100644 index 00000000..0c84aa6c --- /dev/null +++ b/packages/hightide-utils/src/utils/form-validation/translation.ts @@ -0,0 +1,148 @@ +import type { HightideTranslationEntries } from '../../i18n/translations' +import type { + FormValidationErrorType, + NumberFormValidationErrorType, + SelectionFormValidationErrorType, + StringFormValidationErrorType +} from './FormValidationErrorType' + +type TranslationParameters = + HightideTranslationEntries[K] extends (values: infer Values) => string + ? Values + : undefined + +type FormValidationTranslationKey = + `validationError${Capitalize}` extends keyof HightideTranslationEntries + ? `validationError${Capitalize}` + : never + +export type FormValidationTranslationParameters = + TranslationParameters> + +type OptionalTranslationParameters = + FormValidationTranslationParameters extends undefined + ? undefined + : Partial> + +export type FormValidationTranslationInput = { + type: T, + parameter?: OptionalTranslationParameters, +} + +export type FormValidationTranslation = { + key: FormValidationTranslationKey, + parameter: FormValidationTranslationParameters, +} + +const formValidationTranslationKeys = { + notEmpty: 'validationErrorNotEmpty', + invalidEmail: 'validationErrorInvalidEmail', + tooLong: 'validationErrorTooLong', + tooShort: 'validationErrorTooShort', + outOfRangeString: 'validationErrorOutOfRangeString', + outOfRangeNumber: 'validationErrorOutOfRangeNumber', + outOfRangeSelectionItems: 'validationErrorOutOfRangeSelectionItems', + tooFewSelectionItems: 'validationErrorTooFewSelectionItems', + tooManySelectionItems: 'validationErrorTooManySelectionItems', +} as const satisfies { [T in FormValidationErrorType]: FormValidationTranslationKey } + +const numberParameterDefaults = { + min: 0, + max: 0, +} + +const fillNumberParameter = ( + required: T, + provided: Partial | undefined +): T => { + const filled = { ...required } + + if (!provided) { + return filled + } + + for (const key of Object.keys(required) as (keyof T)[]) { + const value = provided[key] + if (value !== undefined) { + filled[key] = value + } + } + + return filled +} + +const toTranslation = ( + type: T, + parameter: FormValidationTranslationParameters +): FormValidationTranslation => ({ + key: formValidationTranslationKeys[type], + parameter, +}) as unknown as FormValidationTranslation + +const mapWithNumbers = ( + type: T, + provided: { min?: number, max?: number } | undefined, + required: { min?: number, max?: number } +): FormValidationTranslation => { + return toTranslation(type, fillNumberParameter(required, provided) as FormValidationTranslationParameters) +} + +export const mapStringTranslation = ( + error: FormValidationTranslationInput +): FormValidationTranslation => { + const provided = error.parameter as { min?: number, max?: number } | undefined + + switch (error.type) { + case 'notEmpty': + case 'invalidEmail': + return toTranslation(error.type, undefined as FormValidationTranslationParameters) + case 'tooShort': + return mapWithNumbers(error.type, provided, { min: numberParameterDefaults.min }) + case 'tooLong': + return mapWithNumbers(error.type, provided, { max: numberParameterDefaults.max }) + case 'outOfRangeString': + return mapWithNumbers(error.type, provided, numberParameterDefaults) + default: { + const unsupported: never = error.type + throw new Error(`Unsupported string validation error type: ${unsupported}`) + } + } +} + +export const mapNumberTranslation = ( + error: FormValidationTranslationInput +): FormValidationTranslation => { + const provided = error.parameter as { min?: number, max?: number } | undefined + + switch (error.type) { + case 'notEmpty': + return toTranslation(error.type, undefined as FormValidationTranslationParameters) + case 'outOfRangeNumber': + return mapWithNumbers(error.type, provided, numberParameterDefaults) + default: { + const unsupported: never = error.type + throw new Error(`Unsupported number validation error type: ${unsupported}`) + } + } +} + +export const mapSelectionTranslation = ( + error: FormValidationTranslationInput +): FormValidationTranslation => { + const provided = error.parameter as { min?: number, max?: number } | undefined + + switch (error.type) { + case 'notEmpty': + return toTranslation(error.type, undefined as FormValidationTranslationParameters) + case 'tooFewSelectionItems': + return mapWithNumbers(error.type, provided, { min: numberParameterDefaults.min }) + case 'tooManySelectionItems': + return mapWithNumbers(error.type, provided, { max: numberParameterDefaults.max }) + case 'outOfRangeSelectionItems': + return mapWithNumbers(error.type, provided, numberParameterDefaults) + default: { + const unsupported: never = error.type + throw new Error(`Unsupported selection validation error type: ${unsupported}`) + } + } +} diff --git a/packages/hightide-utils/src/utils/index.ts b/packages/hightide-utils/src/utils/index.ts index 1cf95af1..3c7d5847 100644 --- a/packages/hightide-utils/src/utils/index.ts +++ b/packages/hightide-utils/src/utils/index.ts @@ -1,10 +1,11 @@ export * from './array' export * from './bagFunctions' export * from './builder' +export * from './componentState' export * from './curve' export * from './date' export * from './duration' -export * from './emailValidation' +export * from './form-validation' export * from './enum' export * from './localization' export * from './loopingArray' diff --git a/packages/hightide-utils/src/utils/react.ts b/packages/hightide-utils/src/utils/react.ts index 987f627e..57077ebe 100644 --- a/packages/hightide-utils/src/utils/react.ts +++ b/packages/hightide-utils/src/utils/react.ts @@ -9,7 +9,7 @@ function assignForwardRef(element: T | null, ref?: ForwardedRef) { } } -function assingRefsBuilder(refs: ForwardedRef[]) : (el: T | null) => void { +function assingRefsBuilder(refs: (ForwardedRef | undefined)[]) : (el: T | null) => void { return (element) => { refs.forEach(ref => { assignForwardRef(element, ref) diff --git a/packages/hightide-utils/tests/context/localization/useCreateLocalizationContext.test.ts b/packages/hightide-utils/tests/context/localization/useCreateLocalizationContext.test.ts index 0bd49d6f..306bacef 100644 --- a/packages/hightide-utils/tests/context/localization/useCreateLocalizationContext.test.ts +++ b/packages/hightide-utils/tests/context/localization/useCreateLocalizationContext.test.ts @@ -7,8 +7,8 @@ import type { SupportedLocalesConfig } from '../../../src/context/localization/L describe('useCreateLocalizationContext', () => { const supportedLocales: SupportedLocalesConfig = { - 'en-US': { localName: 'English (US)' }, - 'de-DE': { localName: 'Deutsch' }, + 'en-US': { localeName: 'English (US)' }, + 'de-DE': { localeName: 'Deutsch' }, } test('resolves locale from controlled prop', () => { @@ -110,13 +110,13 @@ describe('useCreateLocalizationContext', () => { const store = useMemoryKeyValueStore() const localesWithDefaults: SupportedLocalesConfig = { 'en-US': { - localName: 'English (US)', + localeName: 'English (US)', defaultStartingWeekday: 'sunday', defaultCalendarType: 'Gregorian', defaultIs24HourFormat: false, }, 'de-DE': { - localName: 'Deutsch', + localeName: 'Deutsch', defaultStartingWeekday: 'monday', defaultIs24HourFormat: true, }, @@ -136,7 +136,7 @@ describe('useCreateLocalizationContext', () => { const store = useMemoryKeyValueStore() const localesWithDefaults: SupportedLocalesConfig = { 'en-US': { - localName: 'English (US)', + localeName: 'English (US)', defaultStartingWeekday: 'sunday', }, } diff --git a/packages/hightide-utils/tests/hooks/state-machine.test.ts b/packages/hightide-utils/tests/hooks/state-machine.test.ts new file mode 100644 index 00000000..30a75a28 --- /dev/null +++ b/packages/hightide-utils/tests/hooks/state-machine.test.ts @@ -0,0 +1,371 @@ +/** @jest-environment jsdom */ + +import { useState } from 'react' +import { act, renderHook } from '@testing-library/react' +import { createStateStore } from '../../src/hooks/state-machine/StateStore' +import { useStateMachine } from '../../src/hooks/state-machine/useStateMachine' +import { useStateStore } from '../../src/hooks/state-machine/useStateStore' +import { useStateStoreBindings } from '../../src/hooks/state-machine/useStateStoreBindings' + +type CountState = { + count: number, +} + +type LabelledState = { + count: number, + label: string, +} + +type CountEvent = + | { type: 'increment' } + | { type: 'add', amount: number } + +const incrementEvent: CountEvent = { type: 'increment' } + +function incrementTransition(state: CountState, event: CountEvent): CountState { + if (event.type === 'increment') { + return { count: state.count + 1 } + } + return { count: state.count + event.amount } +} + +function doubleTransition(state: CountState, event: CountEvent): CountState { + if (event.type === 'increment') { + return { count: state.count * 2 } + } + return { count: state.count + event.amount } +} + +describe('state store', () => { + test('notifies subscribers and skips an identical snapshot', () => { + const store = createStateStore({ count: 0 }) + const listener = jest.fn() + const unsubscribe = store.subscribe(listener) + const next = { count: 1 } + + store.set(next) + + expect(store.getSnapshot()).toBe(next) + expect(listener).toHaveBeenCalledTimes(1) + + store.set(next) + + expect(listener).toHaveBeenCalledTimes(1) + unsubscribe() + store.set({ count: 2 }) + expect(listener).toHaveBeenCalledTimes(1) + expect(store.getSnapshot()).toEqual({ count: 2 }) + }) + + test('keeps one store for the lifetime of the hook', () => { + const { result, rerender } = renderHook(() => useStateStore({ count: 4 })) + const store = result.current + + rerender() + + expect(result.current).toBe(store) + expect(store.getSnapshot()).toEqual({ count: 4 }) + }) +}) + +describe('useStateMachine', () => { + test('suggests the next state and leaves the store unchanged', () => { + const onStateChange = jest.fn() + const onStateEvent = jest.fn() + const { result } = renderHook(() => { + const store = useStateStore({ count: 2 }) + const machine = useStateMachine({ + state: store, + transition: incrementTransition, + onStateChange, + onStateEvent, + }) + return { store, machine } + }) + + act(() => { + result.current.machine.dispatch({ type: 'add', amount: 5 }) + }) + + expect(onStateChange).toHaveBeenCalledWith({ count: 7 }) + expect(onStateEvent).toHaveBeenCalledWith({ type: 'add', amount: 5 }) + expect(result.current.store.getSnapshot()).toEqual({ count: 2 }) + expect(result.current.machine.state).toEqual({ count: 2 }) + }) + + test('reads each committed snapshot during a burst of dispatches', () => { + const seen: number[] = [] + const { result } = renderHook(() => { + const store = useStateStore({ count: 0 }) + const machine = useStateMachine({ + state: store, + transition: incrementTransition, + onStateChange: (next) => { + seen.push(next.count) + store.set(next) + }, + }) + return { store, machine } + }) + + act(() => { + result.current.machine.dispatch(incrementEvent) + result.current.machine.dispatch(incrementEvent) + result.current.machine.dispatch({ type: 'add', amount: 10 }) + }) + + expect(seen).toEqual([1, 2, 12]) + expect(result.current.store.getSnapshot()).toEqual({ count: 12 }) + expect(result.current.machine.state).toEqual({ count: 12 }) + }) + + test('repeats the same snapshot when a burst does not write the store', () => { + const seen: number[] = [] + const { result } = renderHook(() => { + const store = useStateStore({ count: 4 }) + const machine = useStateMachine({ + state: store, + transition: doubleTransition, + onStateChange: (next) => { + seen.push(next.count) + }, + }) + return machine + }) + + act(() => { + result.current.dispatch(incrementEvent) + result.current.dispatch(incrementEvent) + }) + + expect(seen).toEqual([8, 8]) + expect(result.current.state).toEqual({ count: 4 }) + }) + + test('uses the transition from the latest render', () => { + const { result, rerender } = renderHook(({ transition }: { transition: typeof incrementTransition }) => { + const store = useStateStore({ count: 3 }) + const machine = useStateMachine({ + state: store, + transition, + onStateChange: (next) => { + store.set(next) + }, + }) + return { store, machine } + }, { + initialProps: { transition: incrementTransition }, + }) + + act(() => { + result.current.machine.dispatch(incrementEvent) + result.current.machine.dispatch(incrementEvent) + }) + + expect(result.current.store.getSnapshot()).toEqual({ count: 5 }) + + rerender({ transition: doubleTransition }) + + act(() => { + result.current.machine.dispatch(incrementEvent) + result.current.machine.dispatch(incrementEvent) + }) + + expect(result.current.store.getSnapshot()).toEqual({ count: 20 }) + }) +}) + +describe('useStateStoreBindings', () => { + test('writes an uncontrolled key and reports each value during a burst', () => { + const onValueChange = jest.fn() + const { result } = renderHook(() => { + const store = useStateStore({ count: 0, label: 'start' }) + const onStateChange = useStateStoreBindings({ + store, + bindings: { + count: { onValueChange }, + }, + }) + return { store, onStateChange } + }) + + act(() => { + result.current.onStateChange({ count: 1, label: 'start' }) + result.current.onStateChange({ count: 2, label: 'start' }) + result.current.onStateChange({ count: 2, label: 'start' }) + }) + + expect(onValueChange.mock.calls.map((call) => call[0])).toEqual([1, 2]) + expect(result.current.store.getSnapshot()).toEqual({ count: 2, label: 'start' }) + }) + + test('keeps both keys when one bindings object commits them together', () => { + const onCountChange = jest.fn() + const onLabelChange = jest.fn() + const { result } = renderHook(() => { + const store = useStateStore({ count: 0, label: 'a' }) + const onStateChange = useStateStoreBindings({ + store, + bindings: { + count: { onValueChange: onCountChange }, + label: { onValueChange: onLabelChange }, + }, + }) + return { store, onStateChange } + }) + + act(() => { + result.current.onStateChange({ count: 4, label: 'b' }) + result.current.onStateChange({ count: 5, label: 'b' }) + }) + + expect(result.current.store.getSnapshot()).toEqual({ count: 5, label: 'b' }) + expect(onCountChange.mock.calls.map((call) => call[0])).toEqual([4, 5]) + expect(onLabelChange).toHaveBeenCalledWith('b') + }) + + test('drops a key when onValueChange returns false and commits it otherwise', () => { + const onValueChange = jest.fn(() => false) + const { result, rerender } = renderHook(({ value }: { value?: number }) => { + const store = useStateStore({ count: 1, label: 'a' }) + const onStateChange = useStateStoreBindings({ + store, + bindings: { + count: { value, onValueChange }, + }, + }) + return { store, onStateChange } + }, { + initialProps: { value: 1 as number | undefined }, + }) + + act(() => { + result.current.onStateChange({ count: 6, label: 'next' }) + }) + + expect(onValueChange).toHaveBeenCalledTimes(1) + expect(onValueChange).toHaveBeenCalledWith(6) + expect(result.current.store.getSnapshot()).toEqual({ count: 1, label: 'next' }) + + onValueChange.mockReturnValue(undefined) + act(() => { + result.current.onStateChange({ count: 8, label: 'next' }) + }) + + expect(result.current.store.getSnapshot()).toEqual({ count: 8, label: 'next' }) + + onValueChange.mockReturnValue(true) + act(() => { + result.current.onStateChange({ count: 4, label: 'kept' }) + }) + + expect(result.current.store.getSnapshot()).toEqual({ count: 4, label: 'kept' }) + + rerender({ value: 9 }) + + expect(result.current.store.getSnapshot()).toEqual({ count: 9, label: 'kept' }) + expect(onValueChange).toHaveBeenCalledTimes(3) + }) + + test('keeps the original mode when a value switches between defined and undefined', () => { + const error = jest.spyOn(console, 'error').mockImplementation(() => {}) + const onControlledChange = jest.fn() + const onUncontrolledChange = jest.fn() + const { result, rerender } = renderHook(({ + controlled, + uncontrolled, + }: { + controlled?: number, + uncontrolled?: string, + }) => { + const store = useStateStore({ count: 0, label: 'a' }) + const onStateChange = useStateStoreBindings({ + store, + bindings: { + count: { value: controlled, onValueChange: onControlledChange }, + label: { value: uncontrolled, onValueChange: onUncontrolledChange }, + }, + }) + return { store, onStateChange } + }, { + initialProps: { + controlled: 3 as number | undefined, + uncontrolled: undefined as string | undefined, + }, + }) + + expect(result.current.store.getSnapshot()).toEqual({ count: 3, label: 'a' }) + + rerender({ controlled: undefined, uncontrolled: 'kept' }) + + expect(error).toHaveBeenCalledTimes(2) + expect(result.current.store.getSnapshot()).toEqual({ count: 3, label: 'a' }) + + act(() => { + result.current.onStateChange({ count: 7, label: 'b' }) + }) + + expect(result.current.store.getSnapshot()).toEqual({ count: 7, label: 'b' }) + expect(onControlledChange).toHaveBeenCalledWith(7) + expect(onUncontrolledChange).toHaveBeenCalledWith('b') + error.mockRestore() + }) + + test('useState + useStateMachineBinding', () => { + const { result } = renderHook(() => { + const [count, setCount] = useState(0) + const store = useStateStore({ count }) + const onStateChange = useStateStoreBindings({ + store, + bindings: { + count: { value: count, onValueChange: setCount }, + }, + }) + const machine = useStateMachine({ + state: store, + transition: incrementTransition, + onStateChange, + }) + return { count, machine } + }) + + act(() => { + result.current.machine.dispatch(incrementEvent) + result.current.machine.dispatch(incrementEvent) + }) + + expect(result.current.count).toBe(2) + }) + + test('useState + useStateMachineBinding', () => { + const { result } = renderHook(() => { + const [count, setCount] = useState(0) + const store = useStateStore({ count }) + const onStateChange = useStateStoreBindings({ + store, + bindings: { + count: { + value: count, + onValueChange: (next) => { + setCount(next) + return false + }, + }, + }, + }) + const machine = useStateMachine({ + state: store, + transition: incrementTransition, + onStateChange, + }) + return { count, machine } + }) + + act(() => { + result.current.machine.dispatch(incrementEvent) + result.current.machine.dispatch(incrementEvent) + }) + + expect(result.current.count).toBe(1) + }) +}) diff --git a/packages/hightide-utils/tsup.config.ts b/packages/hightide-utils/tsup.config.ts index d4d41668..615543f4 100644 --- a/packages/hightide-utils/tsup.config.ts +++ b/packages/hightide-utils/tsup.config.ts @@ -7,8 +7,10 @@ export default defineConfig({ 'context/translation/index': 'src/context/translation/index.ts', 'context/theme/index': 'src/context/theme/index.ts', 'context/debug/index': 'src/context/debug/index.ts', + 'form/index': 'src/form/index.ts', 'hooks/index': 'src/hooks/index.ts', 'i18n/index': 'src/i18n/index.ts', + 'interfaces/index': 'src/interfaces/index.ts', 'utils/index': 'src/utils/index.ts', }, format: ['cjs', 'esm'], @@ -22,6 +24,7 @@ export default defineConfig({ external: [ 'react', '@helpwave/internationalization', + 'zustand', ], cjsInterop: true, }) diff --git a/packages/hightide-web/.storybook/main.ts b/packages/hightide-web/.storybook/main.ts index 5c677c75..184510e2 100644 --- a/packages/hightide-web/.storybook/main.ts +++ b/packages/hightide-web/.storybook/main.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' -import type { StorybookConfig } from '@storybook/nextjs-vite' +import type { StorybookConfig } from '@storybook/react-vite' const dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/packages/hightide-web/CHANGELOG.md b/packages/hightide-web/CHANGELOG.md index 0a3a15fa..f4635d13 100644 --- a/packages/hightide-web/CHANGELOG.md +++ b/packages/hightide-web/CHANGELOG.md @@ -5,28 +5,148 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.18.0] - 2026-10-13 +## [0.19.0] - unpublished ### Added +- Filter UI lives in `data-filter`. Each type is a `ControllableDOMInputProps` aggregate (`TextFilter`, `NumberFilter`, `DateFilter`, `DateTimeFilter`, `BooleanFilter`, `SingleTagFilter`, `MultiTagsFilter`, `UnknownTypeFilter`) with `StateManager`, `Operator`, and `Parameter` where needed. `FilterList`, `SortingList`, and the type popups live in `data-filter/interaction` +- Data-input components are grouped by the value they capture: `boolean`, `text`, `number`, `file`, `selection`, and `dateTime`. Stories use the same folders. `InputComponentInterface` is removed. Every data-input aggregate uses `ControllableInputProps`, every data-input state manager uses `InputStateMangerProps`, and other state managers use `StateManagerProps` +- Each of those inputs is an aggregate with `StateManager`, parts, `Context`, `Provider`, and `Consumer`. Folder indexes export the aggregate and types only +- `TabSwitcher` `variant` `'underline' | 'segmented-chips'`, default `'underline'`. The list and each item set `data-style-variant` to that value - `ExpandableHeader` `triggerProps` for the header toggle button - `ExpandableContent` `isClosingOnClick` to collapse when the content is clicked -- `NavigationItemList` `onClose` so Escape on items can close the parent menu -- Default supported locales and theme name translations for Portuguese (Brazil) (`pt-BR`), Spanish (`es-ES`), French (`fr-FR`), Dutch (`nl-NL`), Italian (`it-IT`), Polish (`pl-PL`), Turkish (`tr-TR`), Romanian (`ro-RO`), Greek (`el-GR`), and Russian (`ru-RU`) -- Localized date segment placeholders for the new locales - -### Changed - -- Click and keyboard handlers no longer call `event.stopPropagation` (`Button`, `IconButton`, `Select`, `MultiSelect`, `FileInput`, `Navigation`, `Expandable`, and `PropsUtil` helpers) +- `HorizontalNavigationBar` and `FullScreenVerticalNavigation` aggregates. The bar is a horizontally scrolling row with dropdown groups. Fullscreen navigation is a `StateManager` overlay (`open` / `close` / `toggle`) wrapping `VerticalNavigationMenu` +- `VerticalNavigationMenu` is a folder under `navigation/` (`Item`, `Context`, `Provider`, `Consumer`). Tree expansion context lives with that aggregate; shared item types stay in `navigation/types.ts` +- Navigation styles split into `horizontal-navigation.css`, `vertical-navigation.css`, and `full-screen-vertical-navigation.css` +- Subpath package exports: `@helpwave/hightide/components`, `@helpwave/hightide/global-contexts`, `@helpwave/hightide/hooks`, and `@helpwave/hightide/utils`, each with types +- `Pressable` for colored buttons, with `coloringStyle` (`filled` | `foreground`), `colorVariant` (`normal` | `tonal` | `transparent`), `bordered`, `elevated`, and `processingIndicator` +- `Button` and `IconButton` `variant`: `elevated`, `filled`, `tonal`, `outlined` (`Button` only), and `foreground` +- `surfaceInverse` button color +- `Icon` for a sized icon element (`xs` through `xl`) +- `Divider` with `direction` `horizontal` or `vertical` +- `ListItem`, `ListActionItem`, and `ListNavigationItem` (`href`, `onClick`, or `LinkComponent`; external links show an external icon) +- Compound `Expandable`: `Expandable.Root`, `Expandable.Trigger` (render prop with toggle props), `Expandable.Content`, `Expandable.Context`, and `Expandable.Consumer` +- `ExpandableSection`, `ExpandableSectionHeader`, and `ExpandableSectionContent` for the previous combined expandable layout +- Compound `Avatar`: `Avatar.Root`, `Avatar.Image`, `Avatar.Fallback`, `Avatar.Name`, `Avatar.StatusIndicator`, and `Avatar.Context`. The combined `Avatar` accepts `status`, `hasStatusIndicator`, and slot props +- Compound `AvatarGroup`: `AvatarGroup.Container`, `AvatarGroup.Overlap`, and `AvatarGroup.AdditionalText` +- Compound `WheelPicker` under `data-input/selection`: `WheelPicker.StateManager`, `WheelPicker.Root`, `WheelPicker.Option`, `WheelPicker.Bar`, and `WheelPicker.Context`, with `value` / `initialValue`, `onValueChange`, `onStateEvent`, `onLoop`, `isLooping`, and `visibleRows` +- `DateWheelPicker`, `TimeWheelPicker`, and `DateTimeWheelPicker` +- `ChatAttachmentMessageBubble`, a `ChatMessageBubble` specialization for a file name, metadata, and optional download action +- `ColoringUtils` with `build` (defaults: mode `static`, color variant `normal`, coloring style `filled`) and setters `dataColor`, `dataColoringMode`, `dataColorVariant`, `dataColoringStyle`, `dataBordered`, and `dataElevated` +- Coloring attributes `data-coloring-mode` (`static` | `interactive`), `data-color`, `data-color-variant`, `data-coloring-style`, `data-bordered`, and `data-elevated`. Hover (8%) and press (16%) state layers apply only when mode is `interactive` +- `--coloring-state-hover` and `--coloring-state-press` +- `--appearance-subtle` (`70%`) +- `text-description`, which sets the text color from the active foreground and background +- Added a `ModalPanel` component for the modal panel +- The hightide-utils `locales` folder is copied into `dist` during the build and exported as `@helpwave/hightide/locales/*` + +### Changed + +- Filter state managers share `FilterStateManagerProps` +- `Menu`, `FullScreenVerticalNavigation`, and `AppPage` report open changes through `onIsOpenChange` instead of `onStateChange` +- Data-input state managers take `DOMInputStateMangerProps` and `children` only for state ownership. `initialState` is removed. Aggregates keep that state in `useStateStore` and commit it with `useStateStoreBindings`, so uncontrolled keys stay in the store and controlled keys are reported through `onValueChange` +- `ThemeConfig`, `LocalizationConfig`, and the base `HightideConfig` come from `@helpwave/hightide-utils/context`. Web `HightideConfig` adds `tooltip` +- Default locales and theme metadata use `HightideSupportedLocaleInformation` and `HightideSupportedThemeInformation`. Theme labels use `themeNameLight` / `themeNameDark` +- `Table` and `TableWithSelection` take columns only through `columns`. `TableColumn` and column registration are removed +- `FileInput` is an aggregate with `FileInput.StateManager`, `FileInput.Layout`, `FileInput.Input`, `FileInput.Trigger`, and `FileInput.Menu`. `FileInput.Layout` places the file input and the trigger together +- Data-input aggregates, including `Select`, `MultiSelect`, `Combobox`, `FileInput`, and `LabelledCheckbox`, use `value`, `initialValue`, `onValueChange`, and `onStateEvent`. Completion dispatches `{ type: 'commit', value }`. `isInvalid`, `isDisabled`, `isReadOnly`, and `isRequired` replace `invalid`, `disabled`, `readOnly`, and `required`. `NumberStepperInput` uses `isLooping`, `incrementProps.className`, and `decrementProps.className` +- Property fields (`PropertyBase`, `TextProperty`, `NumberProperty`, `DateProperty`, `CheckboxProperty`, `SingleSelectProperty`, and `MultiSelectProperty`) use `ControllableInputProps`. `required` and `readOnly` are removed. `isRequired` and `isReadOnly` apply to the shell and the input. `isDisabled` or `isReadOnly` locks clear and remove. A field is invalid when `isInvalid` is set, or when it is required and empty. `TextProperty`, `DateProperty`, `SingleSelectProperty`, and `MultiSelectProperty` forward `value`, `initialValue`, `onValueChange`, and `onStateEvent`. `NumberProperty` emits `change` and `commit`. `CheckboxProperty` emits `toggle` and can start from `initialValue`. `PropertyBase` children receive `isRequired` instead of `required` +- `Menu` is a compound component: `Menu.StateManager`, `Menu.Trigger`, `Menu.Content`, `Menu.Item`, `Menu.NavigationItem`, `Menu.Context`, `Menu.Provider`, and `Menu.Consumer`. `Menu.Item` is the action tile and closes the menu when `isClosingOnClick` is set. Open, close, and toggle go through the state manager +- `TabSwitcher` is an aggregate with `TabSwitcher.StateManager`, `TabSwitcher.Layout`, `TabSwitcher.TabList`, `TabSwitcher.TabPanel`, `TabSwitcher.Context`, `TabSwitcher.Provider`, and `TabSwitcher.Consumer`. `TabView` and the panel portal are removed. `activeId`, `onActiveIdChange`, and `initialActiveId` stay the same +- `segmented-chips` uses fully rounded outer corners, slightly rounded inner corners, a primary active item, and neutral inactive items. `underline` keeps the current bottom border +- Form creation and validation use `@helpwave/hightide-utils/form`. `FormField` keeps its current props and bag +- Click and keyboard handlers no longer call `event.stopPropagation` (`Button`, `IconButton`, `Select`, `MultiSelect`, `FileInput`, `HorizontalNavigationBar`, `Expandable`, and `PropsUtil` helpers) - `Button` no longer stops click bubbling; `allowClickEventPropagation` has no effect - `Expandable` header is a ` - ) -} diff --git a/packages/hightide-web/src/components/chat/ChatMessageBubble.tsx b/packages/hightide-web/src/components/chat/ChatMessageBubble.tsx deleted file mode 100644 index d659cff7..00000000 --- a/packages/hightide-web/src/components/chat/ChatMessageBubble.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type { HTMLAttributes, ReactNode } from 'react' -import clsx from 'clsx' -import { CheckCheck } from 'lucide-react' - -export type ChatMessageDirection = 'incoming' | 'outgoing' - -export type ChatMessageBubbleProps = HTMLAttributes & { - direction?: ChatMessageDirection, - timestamp?: ReactNode, - readReceipt?: ReactNode, -} - -export const ChatMessageBubble = ({ - direction = 'incoming', - timestamp, - readReceipt, - children, - ...props -}: ChatMessageBubbleProps) => { - const hasMetaData = timestamp != null || readReceipt != null - - return ( -
-
- {children} -
- {hasMetaData && ( - - {readReceipt != null && ( - - - {readReceipt} - - )} - {timestamp != null && ( - {timestamp} - )} - - )} -
- ) -} diff --git a/packages/hightide-web/src/components/chat/ChatMessageCard.tsx b/packages/hightide-web/src/components/chat/ChatMessageCard.tsx deleted file mode 100644 index 9ab37948..00000000 --- a/packages/hightide-web/src/components/chat/ChatMessageCard.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import type { HTMLAttributes, ReactNode } from 'react' -import clsx from 'clsx' -import type { ChatMessageDirection } from './ChatMessageBubble' -import type { ChipColor } from '../display-and-visualization/Chip' - -export type ChatMessageCardProps = HTMLAttributes & { - icon?: ReactNode, - title: ReactNode, - subtitle?: ReactNode, - badge?: ReactNode, - actions?: ReactNode, - color?: ChipColor, - direction?: ChatMessageDirection, -} - -export const ChatMessageCard = ({ - icon, - title, - subtitle, - badge, - actions, - color = 'primary', - direction = 'incoming', - children, - ...props -}: ChatMessageCardProps) => { - return ( -
-
- {icon && ( - - {icon} - - )} - - {title} - {subtitle && ( - {subtitle} - )} - - {badge && ( - {badge} - )} -
- {children && ( -
{children}
- )} - {actions && ( -
{actions}
- )} -
- ) -} diff --git a/packages/hightide-web/src/components/chat/ChatMessageComposer.tsx b/packages/hightide-web/src/components/chat/ChatMessageComposer.tsx deleted file mode 100644 index 1b351392..00000000 --- a/packages/hightide-web/src/components/chat/ChatMessageComposer.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import type { HTMLAttributes, ReactNode } from 'react' -import { useCallback, useLayoutEffect, useRef } from 'react' -import clsx from 'clsx' -import { SendHorizontal } from 'lucide-react' -import { IconButton } from '../user-interaction/IconButton' -import { useControlledState } from '@helpwave/hightide-utils/hooks' -import { useWindowResizeObserver } from '../../hooks/useWindowResizeObserver' - -const MAX_INPUT_LINES = 7 - -export type ChatMessageComposerProps = Omit, 'onChange'> & { - value?: string, - initialValue?: string, - onValueChange?: (value: string) => void, - onSend: (value: string) => void, - placeholder?: string, - sendLabel?: string, - disabled?: boolean, - actions?: ReactNode, - trailing?: ReactNode, -} - -export const ChatMessageComposer = ({ - value: controlledValue, - initialValue, - onValueChange, - onSend, - placeholder, - sendLabel = 'Send', - disabled = false, - actions, - ...props -}: ChatMessageComposerProps) => { - const textareaRef = useRef(null) - const [value, setValue] = useControlledState({ - value: controlledValue, - onValueChange, - defaultValue: initialValue ?? '', - }) - - const syncTextareaHeight = useCallback(() => { - const textarea = textareaRef.current - if (!textarea) { - return - } - textarea.style.height = '0px' - const lineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight) - const maxHeight = Number.isFinite(lineHeight) ? lineHeight * MAX_INPUT_LINES : textarea.scrollHeight - textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px` - }, []) - - useLayoutEffect(() => { - syncTextareaHeight() - }, [value, syncTextareaHeight]) - - useWindowResizeObserver(syncTextareaHeight) - - const send = () => { - const trimmed = (value ?? '').trim() - if (!trimmed) { - return - } - onSend(trimmed) - setValue('') - } - - return ( -
- {actions && ( -
- {actions} -
- )} -