From a76d3cf861eb631d703092262498720b3a59bced Mon Sep 17 00:00:00 2001 From: Essem Date: Sat, 3 Feb 2024 11:45:45 -0600 Subject: [PATCH 01/14] feat: Add language metadata to notes --- locales/en-US.yml | 1 + .../migration/1706852162173-add-post-lang.js | 13 + .../backend/src/core/NoteCreateService.ts | 12 +- packages/backend/src/core/NoteEditService.ts | 14 + .../src/core/activitypub/ApRendererService.ts | 8 +- .../core/activitypub/models/ApNoteService.ts | 21 + packages/backend/src/core/activitypub/type.ts | 1 + .../src/core/entities/NoteEntityService.ts | 5 +- packages/backend/src/misc/langmap.ts | 620 +++++------------- packages/backend/src/models/Note.ts | 6 + packages/backend/src/models/NoteEdit.ts | 32 +- .../backend/src/models/json-schema/note.ts | 6 + .../src/server/api/endpoints/notes/create.ts | 3 + .../src/server/api/endpoints/notes/edit.ts | 12 +- .../frontend/src/components/MkPostForm.vue | 78 +++ .../src/components/MkPostFormDialog.vue | 3 +- packages/frontend/src/scripts/langmap.ts | 620 +++++------------- packages/frontend/src/store.ts | 4 + packages/misskey-js/src/consts.ts | 93 +++ packages/misskey-js/src/index.ts | 1 + 20 files changed, 630 insertions(+), 923 deletions(-) create mode 100644 packages/backend/migration/1706852162173-add-post-lang.js diff --git a/locales/en-US.yml b/locales/en-US.yml index 64033d6a2..caaada93b 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1265,6 +1265,7 @@ hemisphere: "Where are you located" withSensitive: "Include notes with sensitive files" userSaysSomethingSensitive: "Post by {name} contains sensitive content" enableHorizontalSwipe: "Swipe to switch tabs" +noLanguage: "No language" _bubbleGame: howToPlay: "How to play" _howToPlay: diff --git a/packages/backend/migration/1706852162173-add-post-lang.js b/packages/backend/migration/1706852162173-add-post-lang.js new file mode 100644 index 000000000..90e1338f1 --- /dev/null +++ b/packages/backend/migration/1706852162173-add-post-lang.js @@ -0,0 +1,13 @@ +export class AddPostLang1706852162173 { + name = 'AddPostLang1706852162173' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "lang" character varying(10)`); + await queryRunner.query(`ALTER TABLE "note_edit" ADD "lang" character varying(10)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "lang"`); + await queryRunner.query(`ALTER TABLE "note_edit" DROP COLUMN "lang"`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 35a330533..9f9f6547d 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -63,6 +63,7 @@ import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { langmap } from '@/misc/langmap.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -133,6 +134,7 @@ type Option = { createdAt?: Date | null; name?: string | null; text?: string | null; + lang?: string | null; reply?: MiNote | null; renote?: MiNote | null; files?: MiDriveFile[] | null; @@ -337,6 +339,13 @@ export class NoteCreateService implements OnApplicationShutdown { data.text = null; } + if (data.lang) { + if (!Object.keys(langmap).includes(data.lang.toLowerCase())) throw new Error('invalid param'); + data.lang = data.lang.toLowerCase(); + } else { + data.lang = null; + } + let tags = data.apHashtags; let emojis = data.apEmojis; let mentionedUsers = data.apMentions; @@ -579,6 +588,7 @@ export class NoteCreateService implements OnApplicationShutdown { : null, name: data.name, text: data.text, + lang: data.lang, hasPoll: data.poll != null, cw: data.cw ?? null, tags: tags.map(tag => normalizeForSearch(tag)), @@ -1004,7 +1014,7 @@ export class NoteCreateService implements OnApplicationShutdown { removeOnComplete: true, }); } - + // Pack the note const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 2561bbec2..3d8f52947 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -52,6 +52,7 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { langmap } from '@/misc/langmap.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited'; @@ -122,6 +123,7 @@ type Option = { createdAt?: Date | null; name?: string | null; text?: string | null; + lang?: string | null; reply?: MiNote | null; renote?: MiNote | null; files?: MiDriveFile[] | null; @@ -358,6 +360,13 @@ export class NoteEditService implements OnApplicationShutdown { data.text = null; } + if (data.lang) { + if (!Object.keys(langmap).includes(data.lang.toLowerCase())) throw new Error('invalid param'); + data.lang = data.lang.toLowerCase(); + } else { + data.lang = null; + } + let tags = data.apHashtags; let emojis = data.apEmojis; let mentionedUsers = data.apMentions; @@ -420,6 +429,9 @@ export class NoteEditService implements OnApplicationShutdown { if (oldnote.hasPoll !== !!data.poll) { update.hasPoll = !!data.poll; } + if (data.lang !== oldnote.lang) { + update.lang = data.lang; + } const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id }); @@ -434,6 +446,7 @@ export class NoteEditService implements OnApplicationShutdown { oldText: oldnote.text || undefined, newText: update.text || undefined, cw: update.cw || undefined, + lang: update.lang || undefined, fileIds: undefined, oldDate: exists ? oldnote.updatedAt as Date : this.idService.parse(oldnote.id).date, updatedAt: new Date(), @@ -453,6 +466,7 @@ export class NoteEditService implements OnApplicationShutdown { : null, name: data.name, text: data.text, + lang: data.lang, hasPoll: data.poll != null, cw: data.cw ?? null, tags: tags.map(tag => normalizeForSearch(tag)), diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index a84feffac..5af8fed2a 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -285,7 +285,7 @@ export class ApRendererService { if (instance && instance.softwareName === 'pleroma') isMastodon = true; } } - + const object: ILike = { type: 'Like', id: `${this.config.url}/likes/${noteReaction.id}`, @@ -454,6 +454,9 @@ export class ApRendererService { mediaType: 'text/x.misskeymarkdown', }, }), + contentMap: note.lang ? { + [note.lang]: content, + } : null, _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, @@ -746,6 +749,9 @@ export class ApRendererService { mediaType: 'text/x.misskeymarkdown', }, }), + contentMap: note.lang ? { + [note.lang]: content, + } : null, _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 624ead52f..d2f5f124f 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -25,6 +25,7 @@ import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; +import { langmap } from '@/misc/langmap.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; @@ -244,12 +245,21 @@ export class ApNoteService { let text: string | null = null; if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { text = note.source.content; + } else if (note.contentMap != null) { + const entry = Object.entries(note.contentMap)[0]; + text = this.apMfmService.htmlToMfm(entry[1], note.tag); } else if (typeof note._misskey_content !== 'undefined') { text = note._misskey_content; } else if (typeof note.content === 'string') { text = this.apMfmService.htmlToMfm(note.content, note.tag); } + let lang: string | null = null; + if (note.contentMap != null) { + const key = Object.keys(note.contentMap)[0].toLowerCase(); + lang = Object.keys(langmap).includes(key) ? key : null; + } + // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); @@ -290,6 +300,7 @@ export class ApNoteService { name: note.name, cw, text, + lang, localOnly: false, visibility, visibleUsers, @@ -452,12 +463,21 @@ export class ApNoteService { let text: string | null = null; if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { text = note.source.content; + } else if (note.contentMap != null) { + const entry = Object.entries(note.contentMap)[0]; + text = this.apMfmService.htmlToMfm(entry[1], note.tag); } else if (typeof note._misskey_content !== 'undefined') { text = note._misskey_content; } else if (typeof note.content === 'string') { text = this.apMfmService.htmlToMfm(note.content, note.tag); } + let lang: string | null = null; + if (note.contentMap != null) { + const key = Object.keys(note.contentMap)[0].toLowerCase(); + lang = Object.keys(langmap).includes(key) ? key : null; + } + // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); @@ -498,6 +518,7 @@ export class ApNoteService { name: note.name, cw, text, + lang, localOnly: false, visibility, visibleUsers, diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 716515840..00deddd43 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -21,6 +21,7 @@ export interface IObject { inReplyTo?: any; replies?: ICollection; content?: string | null; + contentMap?: Obj | null; startTime?: Date; endTime?: Date; icon?: any; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 86a8670f2..465b5c28c 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -17,12 +17,12 @@ import { bindThis } from '@/decorators.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; +import type { Config } from '@/config.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; -import type { Config } from '@/config.js'; @Injectable() export class NoteEntityService implements OnModuleInit { @@ -120,7 +120,7 @@ export class NoteEntityService implements OnModuleInit { followerId: meId, }, }); - + hide = !isFollowing; } else { // フォロワーかどうか @@ -373,6 +373,7 @@ export class NoteEntityService implements OnModuleInit { uri: note.uri ?? undefined, url: note.url ?? undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, + lang: note.lang, ...(meId && Object.keys(note.reactions).length > 0 ? { myReaction: this.populateMyReaction(note, meId, options?._hint_), } : {}), diff --git a/packages/backend/src/misc/langmap.ts b/packages/backend/src/misc/langmap.ts index 5ff933865..1d2ec2821 100644 --- a/packages/backend/src/misc/langmap.ts +++ b/packages/backend/src/misc/langmap.ts @@ -4,668 +4,384 @@ */ // TODO: sharedに置いてフロントエンドのと統合したい -export const langmap = { - 'ach': { - nativeName: 'Lwo', - }, - 'ady': { - nativeName: 'Адыгэбзэ', - }, - 'af': { +export const iso639Langs1 = { + af: { nativeName: 'Afrikaans', }, - 'af-NA': { - nativeName: 'Afrikaans (Namibia)', - }, - 'af-ZA': { - nativeName: 'Afrikaans (South Africa)', - }, - 'ak': { + ak: { nativeName: 'Tɕɥi', }, - 'ar': { + ar: { nativeName: 'العربية', + rtl: true, }, - 'ar-AR': { - nativeName: 'العربية', - }, - 'ar-MA': { - nativeName: 'العربية', - }, - 'ar-SA': { - nativeName: 'العربية (السعودية)', - }, - 'ay-BO': { + ay: { nativeName: 'Aymar aru', }, - 'az': { + az: { nativeName: 'Azərbaycan dili', }, - 'az-AZ': { - nativeName: 'Azərbaycan dili', - }, - 'be-BY': { + be: { nativeName: 'Беларуская', }, - 'bg': { + bg: { nativeName: 'Български', }, - 'bg-BG': { - nativeName: 'Български', - }, - 'bn': { + bn: { nativeName: 'বাংলা', }, - 'bn-IN': { - nativeName: 'বাংলা (ভারত)', - }, - 'bn-BD': { - nativeName: 'বাংলা(বাংলাদেশ)', - }, - 'br': { + br: { nativeName: 'Brezhoneg', }, - 'bs-BA': { + bs: { nativeName: 'Bosanski', }, - 'ca': { + ca: { nativeName: 'Català', }, - 'ca-ES': { - nativeName: 'Català', - }, - 'cak': { - nativeName: 'Maya Kaqchikel', - }, - 'ck-US': { - nativeName: 'ᏣᎳᎩ (tsalagi)', - }, - 'cs': { + cs: { nativeName: 'Čeština', }, - 'cs-CZ': { - nativeName: 'Čeština', - }, - 'cy': { + cy: { nativeName: 'Cymraeg', }, - 'cy-GB': { - nativeName: 'Cymraeg', - }, - 'da': { + da: { nativeName: 'Dansk', }, - 'da-DK': { - nativeName: 'Dansk', - }, - 'de': { + de: { nativeName: 'Deutsch', }, - 'de-AT': { - nativeName: 'Deutsch (Österreich)', - }, - 'de-DE': { - nativeName: 'Deutsch (Deutschland)', - }, - 'de-CH': { - nativeName: 'Deutsch (Schweiz)', - }, - 'dsb': { - nativeName: 'Dolnoserbšćina', - }, - 'el': { + el: { nativeName: 'Ελληνικά', }, - 'el-GR': { - nativeName: 'Ελληνικά', - }, - 'en': { + en: { nativeName: 'English', }, - 'en-GB': { - nativeName: 'English (UK)', - }, - 'en-AU': { - nativeName: 'English (Australia)', - }, - 'en-CA': { - nativeName: 'English (Canada)', - }, - 'en-IE': { - nativeName: 'English (Ireland)', - }, - 'en-IN': { - nativeName: 'English (India)', - }, - 'en-PI': { - nativeName: 'English (Pirate)', - }, - 'en-SG': { - nativeName: 'English (Singapore)', - }, - 'en-UD': { - nativeName: 'English (Upside Down)', - }, - 'en-US': { - nativeName: 'English (US)', - }, - 'en-ZA': { - nativeName: 'English (South Africa)', - }, - 'en@pirate': { - nativeName: 'English (Pirate)', - }, - 'eo': { + eo: { nativeName: 'Esperanto', }, - 'eo-EO': { - nativeName: 'Esperanto', - }, - 'es': { + es: { nativeName: 'Español', }, - 'es-AR': { - nativeName: 'Español (Argentine)', - }, - 'es-419': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-CL': { - nativeName: 'Español (Chile)', - }, - 'es-CO': { - nativeName: 'Español (Colombia)', - }, - 'es-EC': { - nativeName: 'Español (Ecuador)', - }, - 'es-ES': { - nativeName: 'Español (España)', - }, - 'es-LA': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-NI': { - nativeName: 'Español (Nicaragua)', - }, - 'es-MX': { - nativeName: 'Español (México)', - }, - 'es-US': { - nativeName: 'Español (Estados Unidos)', - }, - 'es-VE': { - nativeName: 'Español (Venezuela)', - }, - 'et': { + et: { nativeName: 'eesti keel', }, - 'et-EE': { - nativeName: 'Eesti (Estonia)', - }, - 'eu': { + eu: { nativeName: 'Euskara', }, - 'eu-ES': { - nativeName: 'Euskara', - }, - 'fa': { + fa: { nativeName: 'فارسی', + rtl: true, }, - 'fa-IR': { - nativeName: 'فارسی', - }, - 'fb-LT': { - nativeName: 'Leet Speak', - }, - 'ff': { + ff: { nativeName: 'Fulah', }, - 'fi': { + fi: { nativeName: 'Suomi', }, - 'fi-FI': { - nativeName: 'Suomi', - }, - 'fo': { + fo: { nativeName: 'Føroyskt', }, - 'fo-FO': { - nativeName: 'Føroyskt (Færeyjar)', - }, - 'fr': { + fr: { nativeName: 'Français', }, - 'fr-CA': { - nativeName: 'Français (Canada)', - }, - 'fr-FR': { - nativeName: 'Français (France)', - }, - 'fr-BE': { - nativeName: 'Français (Belgique)', - }, - 'fr-CH': { - nativeName: 'Français (Suisse)', - }, - 'fy-NL': { + fy: { nativeName: 'Frysk', }, - 'ga': { + ga: { nativeName: 'Gaeilge', }, - 'ga-IE': { - nativeName: 'Gaeilge', - }, - 'gd': { + gd: { nativeName: 'Gàidhlig', }, - 'gl': { + gl: { nativeName: 'Galego', }, - 'gl-ES': { - nativeName: 'Galego', - }, - 'gn-PY': { + gn: { nativeName: 'Avañe\'ẽ', }, - 'gu-IN': { + gu: { nativeName: 'ગુજરાતી', }, - 'gv': { + gv: { nativeName: 'Gaelg', }, - 'gx-GR': { - nativeName: 'Ἑλληνική ἀρχαία', - }, - 'he': { + he: { nativeName: 'עברית‏', + rtl: true, }, - 'he-IL': { - nativeName: 'עברית‏', - }, - 'hi': { + hi: { nativeName: 'हिन्दी', }, - 'hi-IN': { - nativeName: 'हिन्दी', - }, - 'hr': { + hr: { nativeName: 'Hrvatski', }, - 'hr-HR': { - nativeName: 'Hrvatski', - }, - 'hsb': { - nativeName: 'Hornjoserbšćina', - }, - 'ht': { + ht: { nativeName: 'Kreyòl', }, - 'hu': { + hu: { nativeName: 'Magyar', }, - 'hu-HU': { - nativeName: 'Magyar', - }, - 'hy': { + hy: { nativeName: 'Հայերեն', }, - 'hy-AM': { - nativeName: 'Հայերեն (Հայաստան)', - }, - 'id': { + id: { nativeName: 'Bahasa Indonesia', }, - 'id-ID': { - nativeName: 'Bahasa Indonesia', - }, - 'is': { + is: { nativeName: 'Íslenska', }, - 'is-IS': { - nativeName: 'Íslenska (Iceland)', - }, - 'it': { + it: { nativeName: 'Italiano', }, - 'it-IT': { - nativeName: 'Italiano', - }, - 'ja': { + ja: { nativeName: '日本語', }, - 'ja-JP': { - nativeName: '日本語 (日本)', - }, - 'jv-ID': { + jv: { nativeName: 'Basa Jawa', }, - 'ka-GE': { + ka: { nativeName: 'ქართული', }, - 'kk-KZ': { + kk: { nativeName: 'Қазақша', }, - 'km': { - nativeName: 'ភាសាខ្មែរ', - }, - 'kl': { + kl: { nativeName: 'kalaallisut', }, - 'km-KH': { + km: { nativeName: 'ភាសាខ្មែរ', }, - 'kab': { - nativeName: 'Taqbaylit', - }, - 'kn': { + kn: { nativeName: 'ಕನ್ನಡ', }, - 'kn-IN': { - nativeName: 'ಕನ್ನಡ (India)', - }, - 'ko': { + ko: { nativeName: '한국어', }, - 'ko-KR': { - nativeName: '한국어 (한국)', - }, - 'ku-TR': { + ku: { nativeName: 'Kurdî', }, - 'kw': { + kw: { nativeName: 'Kernewek', }, - 'la': { + la: { nativeName: 'Latin', }, - 'la-VA': { - nativeName: 'Latin', - }, - 'lb': { + lb: { nativeName: 'Lëtzebuergesch', }, - 'li-NL': { + li: { nativeName: 'Lèmbörgs', }, - 'lt': { + lt: { nativeName: 'Lietuvių', }, - 'lt-LT': { - nativeName: 'Lietuvių', - }, - 'lv': { + lv: { nativeName: 'Latviešu', }, - 'lv-LV': { - nativeName: 'Latviešu', - }, - 'mai': { - nativeName: 'मैथिली, মৈথিলী', - }, - 'mg-MG': { + mg: { nativeName: 'Malagasy', }, - 'mk': { + mk: { nativeName: 'Македонски', }, - 'mk-MK': { - nativeName: 'Македонски (Македонски)', - }, - 'ml': { + ml: { nativeName: 'മലയാളം', }, - 'ml-IN': { - nativeName: 'മലയാളം', - }, - 'mn-MN': { + mn: { nativeName: 'Монгол', }, - 'mr': { + mr: { nativeName: 'मराठी', }, - 'mr-IN': { - nativeName: 'मराठी', - }, - 'ms': { + ms: { nativeName: 'Bahasa Melayu', }, - 'ms-MY': { - nativeName: 'Bahasa Melayu', - }, - 'mt': { + mt: { nativeName: 'Malti', }, - 'mt-MT': { - nativeName: 'Malti', - }, - 'my': { + my: { nativeName: 'ဗမာစကာ', }, - 'no': { + no: { nativeName: 'Norsk', }, - 'nb': { + nb: { nativeName: 'Norsk (bokmål)', }, - 'nb-NO': { - nativeName: 'Norsk (bokmål)', - }, - 'ne': { + ne: { nativeName: 'नेपाली', }, - 'ne-NP': { - nativeName: 'नेपाली', - }, - 'nl': { + nl: { nativeName: 'Nederlands', }, - 'nl-BE': { - nativeName: 'Nederlands (België)', - }, - 'nl-NL': { - nativeName: 'Nederlands (Nederland)', - }, - 'nn-NO': { + nn: { nativeName: 'Norsk (nynorsk)', }, - 'oc': { + oc: { nativeName: 'Occitan', }, - 'or-IN': { + or: { nativeName: 'ଓଡ଼ିଆ', }, - 'pa': { + pa: { nativeName: 'ਪੰਜਾਬੀ', }, - 'pa-IN': { - nativeName: 'ਪੰਜਾਬੀ (ਭਾਰਤ ਨੂੰ)', - }, - 'pl': { + pl: { nativeName: 'Polski', }, - 'pl-PL': { - nativeName: 'Polski', - }, - 'ps-AF': { + ps: { nativeName: 'پښتو', + rtl: true, }, - 'pt': { + pt: { nativeName: 'Português', }, - 'pt-BR': { - nativeName: 'Português (Brasil)', - }, - 'pt-PT': { - nativeName: 'Português (Portugal)', - }, - 'qu-PE': { + qu: { nativeName: 'Qhichwa', }, - 'rm-CH': { + rm: { nativeName: 'Rumantsch', }, - 'ro': { + ro: { nativeName: 'Română', }, - 'ro-RO': { - nativeName: 'Română', - }, - 'ru': { + ru: { nativeName: 'Русский', }, - 'ru-RU': { - nativeName: 'Русский', - }, - 'sa-IN': { + sa: { nativeName: 'संस्कृतम्', }, - 'se-NO': { + se: { nativeName: 'Davvisámegiella', }, - 'sh': { + sh: { nativeName: 'српскохрватски', }, - 'si-LK': { + si: { nativeName: 'සිංහල', }, - 'sk': { + sk: { nativeName: 'Slovenčina', }, - 'sk-SK': { - nativeName: 'Slovenčina (Slovakia)', - }, - 'sl': { + sl: { nativeName: 'Slovenščina', }, - 'sl-SI': { - nativeName: 'Slovenščina', - }, - 'so-SO': { + so: { nativeName: 'Soomaaliga', }, - 'sq': { + sq: { nativeName: 'Shqip', }, - 'sq-AL': { - nativeName: 'Shqip', - }, - 'sr': { + sr: { nativeName: 'Српски', }, - 'sr-RS': { - nativeName: 'Српски (Serbia)', - }, - 'su': { + su: { nativeName: 'Basa Sunda', }, - 'sv': { + sv: { nativeName: 'Svenska', }, - 'sv-SE': { - nativeName: 'Svenska', - }, - 'sw': { + sw: { nativeName: 'Kiswahili', }, - 'sw-KE': { - nativeName: 'Kiswahili', - }, - 'ta': { + ta: { nativeName: 'தமிழ்', }, - 'ta-IN': { - nativeName: 'தமிழ்', - }, - 'te': { + te: { nativeName: 'తెలుగు', }, - 'te-IN': { - nativeName: 'తెలుగు', - }, - 'tg': { + tg: { nativeName: 'забо́ни тоҷикӣ́', }, - 'tg-TJ': { - nativeName: 'тоҷикӣ', - }, - 'th': { + th: { nativeName: 'ภาษาไทย', }, - 'th-TH': { - nativeName: 'ภาษาไทย (ประเทศไทย)', - }, - 'fil': { - nativeName: 'Filipino', - }, - 'tlh': { - nativeName: 'tlhIngan-Hol', - }, - 'tr': { + tr: { nativeName: 'Türkçe', }, - 'tr-TR': { - nativeName: 'Türkçe', - }, - 'tt-RU': { + tt: { nativeName: 'татарча', }, - 'uk': { + uk: { nativeName: 'Українська', }, - 'uk-UA': { - nativeName: 'Українська', - }, - 'ur': { + ur: { nativeName: 'اردو', + rtl: true, }, - 'ur-PK': { - nativeName: 'اردو', - }, - 'uz': { + uz: { nativeName: 'O\'zbek', }, - 'uz-UZ': { - nativeName: 'O\'zbek', - }, - 'vi': { + vi: { nativeName: 'Tiếng Việt', }, - 'vi-VN': { - nativeName: 'Tiếng Việt', - }, - 'xh-ZA': { + xh: { nativeName: 'isiXhosa', }, - 'yi': { + yi: { nativeName: 'ייִדיש', + rtl: true, }, - 'yi-DE': { - nativeName: 'ייִדיש (German)', - }, - 'zh': { + zh: { nativeName: '中文', }, - 'zh-Hans': { - nativeName: '中文简体', - }, - 'zh-Hant': { - nativeName: '中文繁體', - }, - 'zh-CN': { - nativeName: '中文(中国大陆)', - }, - 'zh-HK': { - nativeName: '中文(香港)', - }, - 'zh-SG': { - nativeName: '中文(新加坡)', - }, - 'zh-TW': { - nativeName: '中文(台灣)', - }, - 'zu-ZA': { + zu: { nativeName: 'isiZulu', }, }; + +export const iso639Langs3 = { + ach: { + nativeName: 'Lwo', + }, + ady: { + nativeName: 'Адыгэбзэ', + }, + cak: { + nativeName: 'Maya Kaqchikel', + }, + chr: { + nativeName: 'ᏣᎳᎩ (tsalagi)', + }, + dsb: { + nativeName: 'Dolnoserbšćina', + }, + fil: { + nativeName: 'Filipino', + }, + hsb: { + nativeName: 'Hornjoserbšćina', + }, + kab: { + nativeName: 'Taqbaylit', + }, + mai: { + nativeName: 'मैथिली, মৈথিলী', + }, + tlh: { + nativeName: 'tlhIngan-Hol', + }, + tok: { + nativeName: 'Toki Pona', + }, + yue: { + nativeName: '粵語', + }, + nan: { + nativeName: '閩南語', + }, +}; + +export const langmapNoRegion = Object.assign({}, iso639Langs1, iso639Langs3); + +export const iso639Regional = { + 'zh-hans': { + nativeName: '中文(简体)', + }, + 'zh-hant': { + nativeName: '中文(繁體)', + }, +}; + +export const langmap = Object.assign({}, langmapNoRegion, iso639Regional); diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index b11e2ec62..c1cf45e5f 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -61,6 +61,12 @@ export class MiNote { }) public text: string | null; + @Column('varchar', { + length: 10, + nullable: true, + }) + public lang: string | null; + @Column('varchar', { length: 256, nullable: true, }) diff --git a/packages/backend/src/models/NoteEdit.ts b/packages/backend/src/models/NoteEdit.ts index 9ec989dd5..942ba6a0f 100644 --- a/packages/backend/src/models/NoteEdit.ts +++ b/packages/backend/src/models/NoteEdit.ts @@ -1,4 +1,4 @@ -import { Entity, JoinColumn, Column, ManyToOne, PrimaryColumn, Index } from "typeorm"; +import { Entity, JoinColumn, Column, ManyToOne, PrimaryColumn, Index } from 'typeorm'; import { id } from './util/id.js'; import { MiNote } from './Note.js'; import type { MiDriveFile } from './DriveFile.js'; @@ -11,46 +11,52 @@ export class NoteEdit { @Index() @Column({ ...id(), - comment: "The ID of note.", + comment: 'The ID of note.', }) - public noteId: MiNote["id"]; + public noteId: MiNote['id']; @ManyToOne((type) => MiNote, { - onDelete: "CASCADE", + onDelete: 'CASCADE', }) @JoinColumn() public note: MiNote | null; - @Column("text", { + @Column('text', { nullable: true, }) public oldText: string | null; - @Column("text", { + @Column('text', { nullable: true, }) public newText: string | null; - @Column("varchar", { + @Column('varchar', { length: 512, nullable: true, }) public cw: string | null; + @Column('varchar', { + length: 10, + nullable: true, + }) + public lang: string | null; + @Column({ ...id(), array: true, - default: "{}", + default: '{}', }) - public fileIds: MiDriveFile["id"][]; + public fileIds: MiDriveFile['id'][]; - @Column("timestamp with time zone", { - comment: "The updated date of the Note.", + @Column('timestamp with time zone', { + comment: 'The updated date of the Note.', }) public updatedAt: Date; - @Column("timestamp with time zone", { - comment: "The old date from before the edit", + @Column('timestamp with time zone', { + comment: 'The old date from before the edit', }) public oldDate: Date; } diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index bb4ccc7ee..766ff411f 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -2,6 +2,7 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ +import { langmap } from '@/misc/langmap.js'; export const packedNoteSchema = { type: 'object', @@ -26,6 +27,11 @@ export const packedNoteSchema = { type: 'string', optional: false, nullable: true, }, + lang: { + type: 'string', + enum: [...Object.keys(langmap)], + nullable: true, + }, cw: { type: 'string', optional: true, nullable: true, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index aff386709..3c6714235 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -20,6 +20,7 @@ import { isPureRenote } from '@/misc/is-pure-renote.js'; import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { langmap } from '@/misc/langmap.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -136,6 +137,7 @@ export const paramDef = { visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, + lang: { type: 'string', enum: Object.keys(langmap), nullable: true, maxLength: 10 }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, @@ -370,6 +372,7 @@ export default class extends Endpoint { // eslint- expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, } : undefined, text: ps.text ?? undefined, + lang: ps.lang, reply, renote, cw: ps.cw, diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 590853e9c..e12c088e3 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -12,6 +12,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEditService } from '@/core/NoteEditService.js'; import { DI } from '@/di-symbols.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { langmap } from '@/misc/langmap.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -164,6 +165,7 @@ export const paramDef = { format: 'misskey:id', }, }, + lang: { type: 'string', enum: Object.keys(langmap), nullable: true, maxLength: 10 }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, @@ -301,7 +303,7 @@ export default class extends Endpoint { // eslint- if (ps.renoteId === ps.editId) { throw new ApiError(meta.errors.cannotQuoteCurrentPost); } - + if (ps.renoteId != null) { // Fetch renote to note renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); @@ -378,6 +380,13 @@ export default class extends Endpoint { // eslint- } } + if (ps.lang) { + if (!Object.keys(langmap).includes(ps.lang.toLowerCase())) throw new Error('invalid param'); + ps.lang = ps.lang.toLowerCase(); + } else { + ps.lang = null; + } + let channel: MiChannel | null = null; if (ps.channelId != null) { channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false }); @@ -396,6 +405,7 @@ export default class extends Endpoint { // eslint- expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, } : undefined, text: ps.text ?? undefined, + lang: ps.lang, reply, renote, cw: ps.cw, diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index aad2f8087..682a9139e 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -32,6 +32,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ channel.name }} + - +
-