feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
57
packages/i18n/src/utils/index.ts
Normal file
57
packages/i18n/src/utils/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { logger } from "@turbostarter/shared/logger";
|
||||
|
||||
import { config } from "../config";
|
||||
import { translations } from "../translations";
|
||||
|
||||
import type { TranslationKey } from "../client";
|
||||
import type { DefaultNamespace, Namespace, i18n } from "i18next";
|
||||
|
||||
export const loadTranslation = async (
|
||||
locale: (typeof config.locales)[number],
|
||||
namespace: (typeof config.namespaces)[number],
|
||||
) => {
|
||||
try {
|
||||
const data = await translations[locale][namespace]();
|
||||
|
||||
return data.default;
|
||||
} catch {
|
||||
logger.error(`Error while loading i18n file: ${locale}/${namespace}.json`);
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const isKey = <T extends Namespace = DefaultNamespace>(
|
||||
key: string,
|
||||
i18n?: i18n,
|
||||
ns?: T,
|
||||
): key is TranslationKey<T> => {
|
||||
return i18n?.exists(key, { ns }) ?? false;
|
||||
};
|
||||
|
||||
export const isLocaleSupported = (
|
||||
locale: string,
|
||||
): locale is (typeof config.locales)[number] => config.locales.includes(locale);
|
||||
|
||||
export const getPathname = ({
|
||||
locale,
|
||||
path,
|
||||
defaultLocale,
|
||||
}: {
|
||||
locale: string;
|
||||
path: string;
|
||||
defaultLocale?: string;
|
||||
}) => {
|
||||
const pathname = path.replace(
|
||||
new RegExp(`^/(${config.locales.join("|")})`),
|
||||
"",
|
||||
);
|
||||
|
||||
if (locale === (defaultLocale ?? config.defaultLocale)) {
|
||||
return pathname.startsWith("/") ? pathname : `/${pathname}`;
|
||||
}
|
||||
|
||||
return `/${locale}${pathname.startsWith("/") ? pathname : `/${pathname}`}`;
|
||||
};
|
||||
|
||||
export * from "./validation";
|
||||
378
packages/i18n/src/utils/validation.ts
Normal file
378
packages/i18n/src/utils/validation.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import i18next from "i18next";
|
||||
import en from "zod/v4/locales/en.js";
|
||||
|
||||
import { config } from "../config";
|
||||
|
||||
import type { TFunction } from "i18next";
|
||||
import type { $ZodIssue, $ZodIssueBase, $ZodRawIssue } from "zod/v4/core";
|
||||
|
||||
const defaultErrorMap = en().localeError;
|
||||
|
||||
const jsonStringifyReplacer = (_: string, value: unknown): unknown => {
|
||||
if (typeof value === "bigint") {
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
function joinValues<T extends unknown[]>(array: T, separator = " | "): string {
|
||||
return array
|
||||
.map((val) => (typeof val === "string" ? `'${val}'` : val))
|
||||
.join(separator);
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
|
||||
for (const key in value) {
|
||||
if (!Object.prototype.hasOwnProperty.call(value, key)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getKeyAndValues = (
|
||||
param: unknown,
|
||||
defaultKey: string,
|
||||
): {
|
||||
values: Record<string, unknown>;
|
||||
key: string;
|
||||
} => {
|
||||
if (typeof param === "string") return { key: param, values: {} };
|
||||
|
||||
if (isRecord(param)) {
|
||||
const key =
|
||||
"key" in param && typeof param.key === "string" ? param.key : defaultKey;
|
||||
const values =
|
||||
"values" in param && isRecord(param.values) ? param.values : {};
|
||||
return { key, values };
|
||||
}
|
||||
|
||||
return { key: defaultKey, values: {} };
|
||||
};
|
||||
|
||||
const parsedType = (data: unknown): string => {
|
||||
const t = typeof data;
|
||||
|
||||
switch (t) {
|
||||
case "number": {
|
||||
return Number.isNaN(data) ? "NaN" : "number";
|
||||
}
|
||||
case "object": {
|
||||
if (Array.isArray(data)) {
|
||||
return "array";
|
||||
}
|
||||
if (data === null) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
if (
|
||||
Object.getPrototypeOf(data) !== Object.prototype &&
|
||||
data?.constructor
|
||||
) {
|
||||
return data.constructor.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return t;
|
||||
};
|
||||
|
||||
export type $ZodErrorMap<T extends $ZodIssueBase = $ZodIssue> = (
|
||||
issue: $ZodRawIssue<T>,
|
||||
) => {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
export type MakeZodI18nMap = (options?: ZodI18nMapOptions) => $ZodErrorMap;
|
||||
|
||||
export interface ZodI18nMapOptions {
|
||||
t?: TFunction;
|
||||
handlePath?: HandlePathOption | false;
|
||||
}
|
||||
|
||||
export interface HandlePathOption {
|
||||
context?: string;
|
||||
keyPrefix?: string;
|
||||
}
|
||||
|
||||
const defaultNs = "validation";
|
||||
|
||||
export const makeZodI18nMap: MakeZodI18nMap = (options) => (issue) => {
|
||||
const { t, ns, handlePath } = {
|
||||
t: i18next.t,
|
||||
ns: defaultNs,
|
||||
...options,
|
||||
handlePath:
|
||||
options?.handlePath !== false
|
||||
? {
|
||||
context: "with_path",
|
||||
ns: config.namespaces,
|
||||
keyPrefix: undefined,
|
||||
...options?.handlePath,
|
||||
}
|
||||
: null,
|
||||
} as const;
|
||||
|
||||
const defaultResult = defaultErrorMap(issue);
|
||||
const defaultMessage =
|
||||
typeof defaultResult === "string"
|
||||
? defaultResult
|
||||
: (defaultResult?.message ?? "");
|
||||
|
||||
const path =
|
||||
issue.path && issue.path.length > 0 && !!handlePath
|
||||
? {
|
||||
context: handlePath.context,
|
||||
path: t(
|
||||
[handlePath.keyPrefix, issue.path.join(".")]
|
||||
.filter(Boolean)
|
||||
.join("."),
|
||||
{
|
||||
ns: handlePath.ns,
|
||||
defaultValue: issue.path.join("."),
|
||||
},
|
||||
),
|
||||
}
|
||||
: {};
|
||||
|
||||
switch (issue.code) {
|
||||
case "invalid_type":
|
||||
if (issue.received === undefined || issue.received === null) {
|
||||
const code = `${ns}:error.undefined`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
} else {
|
||||
const parsed = parsedType(issue.input).toLocaleLowerCase();
|
||||
const code = `${ns}:error.type`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
ns,
|
||||
expected: t(`type.${issue.expected}`, {
|
||||
defaultValue: issue.expected,
|
||||
}),
|
||||
received: t(`type.${parsed}`, {
|
||||
defaultValue: parsed,
|
||||
}),
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
case "unrecognized_keys": {
|
||||
const code = `${ns}:error.unrecognizedKeys`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
keys: joinValues(issue.keys, ", "),
|
||||
count: issue.keys.length,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
case "invalid_union": {
|
||||
const code = `${ns}:error.union`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
case "invalid_key": {
|
||||
const code = `${ns}:error.invalidKey`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
|
||||
case "invalid_element": {
|
||||
const code = `${ns}:error.invalidElement`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
origin: issue.origin,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
|
||||
case "invalid_value": {
|
||||
const code = `${ns}:error.invalidValue`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
values: joinValues(issue.values, ", "),
|
||||
count: issue.values.length,
|
||||
expected: JSON.stringify(issue.values, jsonStringifyReplacer),
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
|
||||
case "too_small": {
|
||||
const minimum =
|
||||
issue.origin === "date"
|
||||
? new Date(issue.minimum as number)
|
||||
: issue.minimum;
|
||||
|
||||
const code = `${ns}:error.tooSmall.${issue.origin}.${
|
||||
issue.exact ? "exact" : issue.inclusive ? "inclusive" : "notInclusive"
|
||||
}`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
minimum,
|
||||
count: typeof minimum === "number" ? minimum : undefined,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
case "too_big": {
|
||||
const maximum =
|
||||
issue.origin === "date"
|
||||
? new Date(issue.maximum as number)
|
||||
: issue.maximum;
|
||||
|
||||
const code = `${ns}:error.tooBig.${issue.origin}.${
|
||||
issue.exact ? "exact" : issue.inclusive ? "inclusive" : "notInclusive"
|
||||
}`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
maximum,
|
||||
count: typeof maximum === "number" ? maximum : undefined,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
|
||||
case "invalid_format": {
|
||||
if (issue.format === "starts_with") {
|
||||
const code = `${ns}:error.string.startsWith`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
startsWith: issue.prefix,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
} else if (issue.format === "ends_with") {
|
||||
const code = `${ns}:error.string.endsWith`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
endsWith: issue.suffix,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
} else if (issue.format === "includes") {
|
||||
const code = `${ns}:error.string.includes`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
includes: issue.includes,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
};
|
||||
} else if (issue.format === "regex") {
|
||||
const code = `${ns}:error.string.regex`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
pattern: issue.pattern,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
} else {
|
||||
const code = `${ns}:error.string.generic`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
format: issue.format,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case "custom": {
|
||||
const { key, values } = getKeyAndValues(
|
||||
issue.params?.i18n,
|
||||
"error.custom",
|
||||
);
|
||||
|
||||
return {
|
||||
message: t(key, {
|
||||
...values,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code: key,
|
||||
};
|
||||
}
|
||||
case "not_multiple_of": {
|
||||
const code = `${ns}:error.notMultipleOf`;
|
||||
|
||||
return {
|
||||
message: t(code, {
|
||||
multipleOf: issue.multipleOf,
|
||||
ns,
|
||||
defaultValue: defaultMessage,
|
||||
...path,
|
||||
}),
|
||||
code,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return { message: defaultMessage, code: `${ns}:error.default` };
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user