diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index b56e07b..e0fe7a8 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -124,6 +124,9 @@ export const en: Messages = { heading: "Create a Wiki", name: "Name", namePlaceholder: "My Wiki", + slug: "URL slug", + slugHint: "Optional. Defaults to the name. Used in the wiki's address.", + slugPlaceholder: "my-wiki", description: "Description", descriptionPlaceholder: "What is this wiki about?", language: "Language", @@ -223,6 +226,10 @@ export const en: Messages = { routeNotFound: "This page doesn't exist.", wikiNameRequired: "Wiki name is required.", wikiSlugExists: 'A wiki with slug "{slug}" already exists.', + wikiSlugRequired: + "That name can't be turned into a URL. Please choose a URL slug (letters a-z, numbers and hyphens).", + wikiSlugInvalid: + '"{slug}" can\'t be used as a URL slug. Use letters a-z, numbers and hyphens.', wikiLanguageRequired: "Language is required.", wikiLanguageInvalid: "Language must be a valid BCP-47 tag (e.g. en, pt-BR).", diff --git a/src/lib/i18n/fr.ts b/src/lib/i18n/fr.ts index b885784..189f8cb 100644 --- a/src/lib/i18n/fr.ts +++ b/src/lib/i18n/fr.ts @@ -126,6 +126,10 @@ export const fr: PartialMessages = { heading: "Créer un wiki", name: "Nom", namePlaceholder: "Mon wiki", + slug: "Slug d'URL", + slugHint: + "Facultatif. Dérivé du nom par défaut. Utilisé dans l'adresse du wiki.", + slugPlaceholder: "mon-wiki", description: "Description", descriptionPlaceholder: "De quoi parle ce wiki ?", language: "Langue", @@ -226,6 +230,10 @@ export const fr: PartialMessages = { routeNotFound: "Cette page n'existe pas.", wikiNameRequired: "Le nom du wiki est requis.", wikiSlugExists: 'Un wiki avec le slug "{slug}" existe déjà.', + wikiSlugRequired: + "Ce nom ne peut pas être transformé en URL. Choisissez un slug (lettres a-z, chiffres et tirets).", + wikiSlugInvalid: + '"{slug}" ne peut pas servir de slug d\'URL. Utilisez des lettres a-z, des chiffres et des tirets.', wikiLanguageRequired: "La langue est requise.", wikiLanguageInvalid: "La langue doit être une étiquette BCP-47 valide (p. ex. en, pt-BR).", diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index d90e5be..8f637dc 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -122,6 +122,9 @@ export interface Messages { heading: string; name: string; namePlaceholder: string; + slug: string; + slugHint: string; + slugPlaceholder: string; description: string; descriptionPlaceholder: string; language: string; @@ -216,6 +219,8 @@ export interface Messages { routeNotFound: string; wikiNameRequired: string; wikiSlugExists: string; + wikiSlugRequired: string; + wikiSlugInvalid: string; wikiLanguageRequired: string; wikiLanguageInvalid: string; wikiNameTooLong: string; diff --git a/src/lib/i18n/ja.ts b/src/lib/i18n/ja.ts index 83c719f..788b414 100644 --- a/src/lib/i18n/ja.ts +++ b/src/lib/i18n/ja.ts @@ -129,6 +129,10 @@ export const ja: PartialMessages = { heading: "Wiki を作成", name: "名前", namePlaceholder: "My Wiki", + slug: "URL スラッグ", + slugHint: + "任意。未入力の場合は名前から生成されます。Wiki のアドレスに使われます。", + slugPlaceholder: "my-wiki", description: "説明", descriptionPlaceholder: "この Wiki はどんな内容ですか?", language: "言語", @@ -227,6 +231,10 @@ export const ja: PartialMessages = { routeNotFound: "このページは存在しません。", wikiNameRequired: "Wiki 名を入力してください。", wikiSlugExists: "スラッグ「{slug}」の Wiki はすでに存在します。", + wikiSlugRequired: + "この名前は URL に変換できません。URL スラッグ(英小文字・数字・ハイフン)を入力してください。", + wikiSlugInvalid: + "「{slug}」は URL スラッグとして使用できません。英小文字・数字・ハイフンを使用してください。", wikiLanguageRequired: "言語を選択してください。", wikiLanguageInvalid: "言語は有効な BCP-47 タグである必要があります(例: en、pt-BR)。", diff --git a/src/lib/orchestrators/wiki.ts b/src/lib/orchestrators/wiki.ts index ea4ccb3..a71727d 100644 --- a/src/lib/orchestrators/wiki.ts +++ b/src/lib/orchestrators/wiki.ts @@ -38,6 +38,8 @@ export interface WikiFormFields { language: string; visibility: string; description: string; + /** Optional URL slug. Derived from the name when empty. */ + slug?: string; } // Theme name when enforced; undefined in reader mode so the PDS record omits the field. @@ -82,11 +84,18 @@ export async function createWikiCore( throw new ValidationError(msg.error.wikiLanguageInvalid); } - const slug = slugify(fields.name); + // The slug is this record's rkey, and the atproto rkey charset is ASCII-only, + // so it can't follow the name into Unicode the way a note slug can. Names that + // don't transliterate (Japanese, Korean, Chinese) yield "" here, which used to + // be a dead end — now the form can supply one instead. + const requested = fields.slug?.trim(); + const slug = slugify(requested || fields.name); if (!isValidSlug(slug)) { throw new ValidationError( - fmt(msg.error.invalidSlug, { title: fields.name }), + requested + ? fmt(msg.error.wikiSlugInvalid, { slug: requested }) + : msg.error.wikiSlugRequired, ); } diff --git a/src/server/routes/wiki.ts b/src/server/routes/wiki.ts index 7d3bae6..deccb18 100644 --- a/src/server/routes/wiki.ts +++ b/src/server/routes/wiki.ts @@ -118,6 +118,7 @@ export const wikiCreationRoutes = new Elysia({ prefix: "/wiki" }) const visibility = (formData.get("visibility") as string | null) ?? "public"; const description = (formData.get("description") as string | null) ?? ""; + const slug = (formData.get("slug") as string | null) ?? ""; const zipFile = formData.get("zipFile"); const renderError = (error: string) => @@ -126,6 +127,7 @@ export const wikiCreationRoutes = new Elysia({ prefix: "/wiki" }) ...baseLayoutOptions(ctx), error, nameValue: name, + slugValue: slug, languageValue: language, visibilityValue: visibility, descriptionValue: description, @@ -142,7 +144,7 @@ export const wikiCreationRoutes = new Elysia({ prefix: "/wiki" }) const zipBuffer = await zipFile.arrayBuffer(); const { wikiSlug } = await importWikiAction( ctx, - { name, language, visibility, description, zipBuffer }, + { name, language, visibility, description, slug, zipBuffer }, msg, ); return redirect(wikiUrl(ctx.session.handle, wikiSlug)); @@ -150,7 +152,7 @@ export const wikiCreationRoutes = new Elysia({ prefix: "/wiki" }) const { wikiSlug } = await createWikiAction( ctx, - { name, language, visibility, description }, + { name, language, visibility, description, slug }, msg, ); return redirect(wikiUrl(ctx.session.handle, wikiSlug)); diff --git a/src/views/new-wiki.ts b/src/views/new-wiki.ts index 4437b5d..db80adf 100644 --- a/src/views/new-wiki.ts +++ b/src/views/new-wiki.ts @@ -13,6 +13,7 @@ import { interface NewWikiOptions extends LayoutOptions { error?: string; nameValue?: string; + slugValue?: string; descriptionValue?: string; languageValue?: string; visibilityValue?: string; @@ -25,6 +26,7 @@ export function newWikiPage(options?: NewWikiOptions): string { const errorHtml = errorBanner(options?.error); const nameValue = escapeHtml(options?.nameValue ?? ""); + const slugValue = escapeHtml(options?.slugValue ?? ""); const descriptionValue = escapeHtml(options?.descriptionValue ?? ""); const selectedLang = options?.languageValue ?? ""; const selectedVis = options?.visibilityValue ?? "public"; @@ -59,6 +61,18 @@ export function newWikiPage(options?: NewWikiOptions): string { placeholder="${msg.createWiki.namePlaceholder}" > +
${msg.createWiki.slugHint}
+