From d89ca8f44aca94f2b43b719a14a986f9916b1cdf Mon Sep 17 00:00:00 2001 From: juprodh Date: Wed, 8 Jul 2026 22:30:44 +0800 Subject: [PATCH] Add more languages possibilities for wikis --- src/lib/i18n/en.ts | 3 + src/lib/i18n/fr.ts | 3 + src/lib/i18n/index.ts | 3 + src/lib/i18n/ja.ts | 3 + src/lib/languages.ts | 260 ++++++++++++++++++++++++++++++++++++++++-- src/views/new-wiki.ts | 18 ++- 6 files changed, 272 insertions(+), 18 deletions(-) diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index 19f2a03..8cec1b6 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -126,6 +126,9 @@ export const en: Messages = { description: "Description", descriptionPlaceholder: "What is this wiki about?", language: "Language", + languagePlaceholder: "Select a language…", + languageCommon: "Common", + languageAll: "All languages", visibility: "Visibility", privateWarning: "Your wiki will be hidden on Lichen, but the underlying records on your PDS remain publicly accessible. Private data on ATProto is not yet available, fully private wikis will come when the protocol supports it.", diff --git a/src/lib/i18n/fr.ts b/src/lib/i18n/fr.ts index 0ffb4a9..dfc14c4 100644 --- a/src/lib/i18n/fr.ts +++ b/src/lib/i18n/fr.ts @@ -128,6 +128,9 @@ export const fr: PartialMessages = { description: "Description", descriptionPlaceholder: "De quoi parle ce wiki ?", language: "Langue", + languagePlaceholder: "Choisissez une langue…", + languageCommon: "Courantes", + languageAll: "Toutes les langues", visibility: "Visibilité", privateWarning: "Votre wiki sera masqué sur Lichen, mais les enregistrements sur votre PDS restent accessibles publiquement. Les données privées sur ATProto ne sont pas encore disponibles, les wikis entièrement privés arriveront quand le protocole le permettra.", diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index aca232e..fe36332 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -123,6 +123,9 @@ export interface Messages { description: string; descriptionPlaceholder: string; language: string; + languagePlaceholder: string; + languageCommon: string; + languageAll: string; visibility: string; privateWarning: string; create: string; diff --git a/src/lib/i18n/ja.ts b/src/lib/i18n/ja.ts index ff93dd6..6d64bb3 100644 --- a/src/lib/i18n/ja.ts +++ b/src/lib/i18n/ja.ts @@ -131,6 +131,9 @@ export const ja: PartialMessages = { description: "説明", descriptionPlaceholder: "この Wiki はどんな内容ですか?", language: "言語", + languagePlaceholder: "言語を選択…", + languageCommon: "主要な言語", + languageAll: "すべての言語", visibility: "公開範囲", privateWarning: "Wiki は Lichen 上では非表示になりますが、PDS 上の元のレコードは引き続き誰でもアクセスできます。ATProto ではまだ非公開データに対応していないため、完全に非公開の Wiki はプロトコルが対応してからの提供となります。", diff --git a/src/lib/languages.ts b/src/lib/languages.ts index 1e07033..918f783 100644 --- a/src/lib/languages.ts +++ b/src/lib/languages.ts @@ -1,20 +1,211 @@ import { LIMITS } from "./limits.ts"; -export const WIKI_LANGUAGES = [ - { code: "en", label: "English" }, - { code: "fr", label: "Francais" }, - { code: "es", label: "Espanol" }, - { code: "de", label: "Deutsch" }, - { code: "pt", label: "Portugues" }, - { code: "ja", label: "Japanese" }, - { code: "zh", label: "Chinese" }, - { code: "ko", label: "Korean" }, - { code: "ar", label: "Arabic" }, - { code: "eo", label: "Esperanto" }, +// Full ISO 639-1 set (two-letter codes only). Display labels are generated +// at render time via Intl.DisplayNames rather than hand-maintained, so this +// list never needs curation/updates as new languages get added to the picker. +const ISO_639_1_CODES = [ + "aa", + "ab", + "ae", + "af", + "ak", + "am", + "an", + "ar", + "as", + "av", + "ay", + "az", + "ba", + "be", + "bg", + "bh", + "bi", + "bm", + "bn", + "bo", + "br", + "bs", + "ca", + "ce", + "ch", + "co", + "cr", + "cs", + "cu", + "cv", + "cy", + "da", + "de", + "dv", + "dz", + "ee", + "el", + "en", + "eo", + "es", + "et", + "eu", + "fa", + "ff", + "fi", + "fj", + "fo", + "fr", + "fy", + "ga", + "gd", + "gl", + "gn", + "gu", + "gv", + "ha", + "he", + "hi", + "ho", + "hr", + "ht", + "hu", + "hy", + "hz", + "ia", + "id", + "ie", + "ig", + "ii", + "ik", + "io", + "is", + "it", + "iu", + "ja", + "jv", + "ka", + "kg", + "ki", + "kj", + "kk", + "kl", + "km", + "kn", + "ko", + "kr", + "ks", + "ku", + "kv", + "kw", + "ky", + "la", + "lb", + "lg", + "li", + "ln", + "lo", + "lt", + "lu", + "lv", + "mg", + "mh", + "mi", + "mk", + "ml", + "mn", + "mr", + "ms", + "mt", + "my", + "na", + "nb", + "nd", + "ne", + "ng", + "nl", + "nn", + "no", + "nr", + "nv", + "ny", + "oc", + "oj", + "om", + "or", + "os", + "pa", + "pi", + "pl", + "ps", + "pt", + "qu", + "rm", + "rn", + "ro", + "ru", + "rw", + "sa", + "sc", + "sd", + "se", + "sg", + "si", + "sk", + "sl", + "sm", + "sn", + "so", + "sq", + "sr", + "ss", + "st", + "su", + "sv", + "sw", + "ta", + "te", + "tg", + "th", + "ti", + "tk", + "tl", + "tn", + "to", + "tr", + "ts", + "tt", + "tw", + "ty", + "ug", + "uk", + "ur", + "uz", + "ve", + "vi", + "vo", + "wa", + "wo", + "xh", + "yi", + "yo", + "za", + "zh", + "zu", +] as const; + +// Curated set surfaced first in the language picker as a quick-pick group. +const COMMON_LANGUAGE_CODES = [ + "en", + "fr", + "es", + "de", + "pt", + "ja", + "zh", + "ko", + "ar", + "eo", ] as const; // Wikis carry a free-form BCP-47 content-language tag (not limited to the -// dropdown above), so validate by shape rather than a fixed list. This is the +// list above), so validate by shape rather than a fixed list. This is the // write-side half of the defense; views must still escape the value, since // records from third-party PDSes bypass this check entirely. const BCP47_TAG = /^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$/; @@ -22,3 +213,48 @@ const BCP47_TAG = /^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$/; export function isValidLanguageTag(tag: string): boolean { return tag.length <= LIMITS.wiki.language && BCP47_TAG.test(tag); } + +export interface LanguageOption { + code: string; + label: string; +} + +// Groups the ISO 639-1 set for the picker: the curated common codes first (in +// curated order), then the remainder sorted by localized display name. A single +// Intl.DisplayNames instance resolves every label. Labels are for display only — +// the option value is always the code, so submissions can't carry a free-form name. +export function languageOptions(locale: string): { + common: LanguageOption[]; + all: LanguageOption[]; +} { + let display: Intl.DisplayNames | null = null; + try { + display = new Intl.DisplayNames([locale, "en"], { type: "language" }); + } catch { + display = null; + } + const label = (code: string): string => { + let name: string; + try { + name = display?.of(code) ?? code; + } catch { + name = code; + } + // Some locales (e.g. French) return language names lowercase. Uppercase + // the first letter so menu items read consistently in every UI locale. + const [first, ...rest] = [...name]; + return first ? first.toLocaleUpperCase(locale) + rest.join("") : name; + }; + + const common = COMMON_LANGUAGE_CODES.map((code) => ({ + code, + label: label(code), + })); + + const commonSet = new Set(COMMON_LANGUAGE_CODES); + const all = ISO_639_1_CODES.filter((code) => !commonSet.has(code)) + .map((code) => ({ code, label: label(code) })) + .sort((a, b) => a.label.localeCompare(b.label, locale)); + + return { common, all }; +} diff --git a/src/views/new-wiki.ts b/src/views/new-wiki.ts index cb85cf2..3a25cb9 100644 --- a/src/views/new-wiki.ts +++ b/src/views/new-wiki.ts @@ -1,6 +1,6 @@ import { escapeHtml } from "../lib/html.ts"; import { t } from "../lib/i18n/index.ts"; -import { WIKI_LANGUAGES } from "../lib/languages.ts"; +import { type LanguageOption, languageOptions } from "../lib/languages.ts"; import { type LayoutOptions, layout } from "./layout.ts"; import { errorBanner, @@ -29,10 +29,16 @@ export function newWikiPage(options?: NewWikiOptions): string { const selectedLang = options?.languageValue ?? ""; const selectedVis = options?.visibilityValue ?? "public"; - const langOptions = WIKI_LANGUAGES.map( - (l) => - ``, - ).join("\n"); + const { common, all } = languageOptions(locale); + const renderOption = (o: LanguageOption): string => + ``; + const langOptions = ` + + ${common.map(renderOption).join("\n")} + + + ${all.map(renderOption).join("\n")} + `; return layout( msg.createWiki.heading, @@ -87,7 +93,7 @@ export function newWikiPage(options?: NewWikiOptions): string { required class="${inputClass}" > - + ${langOptions} -- 2.51.2