refactor: fully typed locales (#13033)

* refactor: fully typed locales

* refactor: hide parameterized locale strings from type data in ts access

* refactor: missing assertions

* docs: annotation
This commit is contained in:
Acid Chicken (硫酸鶏) 2024-01-19 07:58:07 +09:00 committed by GitHub
parent c1019a006b
commit 43401210c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 383 additions and 165 deletions

View File

@ -6,54 +6,171 @@ import ts from 'typescript';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const parameterRegExp = /\{(\w+)\}/g;
function createMemberType(item) {
if (typeof item !== 'string') {
return ts.factory.createTypeLiteralNode(createMembers(item));
}
const parameters = Array.from(
item.matchAll(parameterRegExp),
([, parameter]) => parameter,
);
if (!parameters.length) {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
}
return ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('ParameterizedString'),
[
ts.factory.createUnionTypeNode(
parameters.map((parameter) =>
ts.factory.createLiteralTypeNode(
ts.factory.createStringLiteral(parameter),
),
),
),
],
);
}
function createMembers(record) { function createMembers(record) {
return Object.entries(record) return Object.entries(record).map(([k, v]) =>
.map(([k, v]) => ts.factory.createPropertySignature( ts.factory.createPropertySignature(
undefined, undefined,
ts.factory.createStringLiteral(k), ts.factory.createStringLiteral(k),
undefined, undefined,
typeof v === 'string' createMemberType(v),
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) ),
: ts.factory.createTypeLiteralNode(createMembers(v)), );
));
} }
export default function generateDTS() { export default function generateDTS() {
const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
const members = createMembers(locale); const members = createMembers(locale);
const elements = [ const elements = [
ts.factory.createVariableStatement(
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createIdentifier('kParameters'),
undefined,
ts.factory.createTypeOperatorNode(
ts.SyntaxKind.UniqueKeyword,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword),
),
undefined,
),
],
ts.NodeFlags.Const,
),
),
ts.factory.createInterfaceDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier('ParameterizedString'),
[
ts.factory.createTypeParameterDeclaration(
undefined,
ts.factory.createIdentifier('T'),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('string'),
undefined,
),
),
],
undefined,
[
ts.factory.createPropertySignature(
undefined,
ts.factory.createComputedPropertyName(
ts.factory.createIdentifier('kParameters'),
),
undefined,
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('T'),
undefined,
),
),
],
),
ts.factory.createInterfaceDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier('ILocale'),
undefined,
undefined,
[
ts.factory.createIndexSignature(
undefined,
[
ts.factory.createParameterDeclaration(
undefined,
undefined,
ts.factory.createIdentifier('_'),
undefined,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
undefined,
),
],
ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('ParameterizedString'),
[ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)],
),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('ILocale'),
undefined,
),
]),
),
],
),
ts.factory.createInterfaceDeclaration( ts.factory.createInterfaceDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier('Locale'), ts.factory.createIdentifier('Locale'),
undefined, undefined,
undefined, [
ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
ts.factory.createExpressionWithTypeArguments(
ts.factory.createIdentifier('ILocale'),
undefined,
),
]),
],
members, members,
), ),
ts.factory.createVariableStatement( ts.factory.createVariableStatement(
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
ts.factory.createVariableDeclarationList( ts.factory.createVariableDeclarationList(
[ts.factory.createVariableDeclaration( [
ts.factory.createIdentifier('locales'), ts.factory.createVariableDeclaration(
undefined, ts.factory.createIdentifier('locales'),
ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature(
undefined, undefined,
[ts.factory.createParameterDeclaration( ts.factory.createTypeLiteralNode([
undefined, ts.factory.createIndexSignature(
undefined, undefined,
ts.factory.createIdentifier('lang'), [
undefined, ts.factory.createParameterDeclaration(
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), undefined,
undefined, undefined,
)], ts.factory.createIdentifier('lang'),
ts.factory.createTypeReferenceNode( undefined,
ts.factory.createIdentifier('Locale'), ts.factory.createKeywordTypeNode(
undefined, ts.SyntaxKind.StringKeyword,
), ),
)]), undefined,
undefined, ),
)], ],
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('Locale'),
undefined,
),
),
]),
undefined,
),
],
ts.NodeFlags.Const,
), ),
), ),
ts.factory.createFunctionDeclaration( ts.factory.createFunctionDeclaration(
@ -70,16 +187,28 @@ export default function generateDTS() {
), ),
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')), ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
]; ];
const printed = ts.createPrinter({ const printed = ts
newLine: ts.NewLineKind.LineFeed, .createPrinter({
}).printList( newLine: ts.NewLineKind.LineFeed,
ts.ListFormat.MultiLine, })
ts.factory.createNodeArray(elements), .printList(
ts.createSourceFile('index.d.ts', '', ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS), ts.ListFormat.MultiLine,
); ts.factory.createNodeArray(elements),
ts.createSourceFile(
'index.d.ts',
'',
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.TS,
),
);
fs.writeFileSync(`${__dirname}/index.d.ts`, `/* eslint-disable */ fs.writeFileSync(
`${__dirname}/index.d.ts`,
`/* eslint-disable */
// This file is generated by locales/generateDTS.js // This file is generated by locales/generateDTS.js
// Do not edit this file directly. // Do not edit this file directly.
${printed}`, 'utf-8'); ${printed}`,
'utf-8',
);
} }

229
locales/index.d.ts vendored
View File

@ -1,12 +1,19 @@
/* eslint-disable */ /* eslint-disable */
// This file is generated by locales/generateDTS.js // This file is generated by locales/generateDTS.js
// Do not edit this file directly. // Do not edit this file directly.
export interface Locale { declare const kParameters: unique symbol;
export interface ParameterizedString<T extends string> {
[kParameters]: T;
}
export interface ILocale {
[_: string]: string | ParameterizedString<string> | ILocale;
}
export interface Locale extends ILocale {
"_lang_": string; "_lang_": string;
"headlineMisskey": string; "headlineMisskey": string;
"introMisskey": string; "introMisskey": string;
"poweredByMisskeyDescription": string; "poweredByMisskeyDescription": ParameterizedString<"name">;
"monthAndDay": string; "monthAndDay": ParameterizedString<"month" | "day">;
"search": string; "search": string;
"notifications": string; "notifications": string;
"username": string; "username": string;
@ -18,7 +25,7 @@ export interface Locale {
"cancel": string; "cancel": string;
"noThankYou": string; "noThankYou": string;
"enterUsername": string; "enterUsername": string;
"renotedBy": string; "renotedBy": ParameterizedString<"user">;
"noNotes": string; "noNotes": string;
"noNotifications": string; "noNotifications": string;
"instance": string; "instance": string;
@ -78,8 +85,8 @@ export interface Locale {
"export": string; "export": string;
"files": string; "files": string;
"download": string; "download": string;
"driveFileDeleteConfirm": string; "driveFileDeleteConfirm": ParameterizedString<"name">;
"unfollowConfirm": string; "unfollowConfirm": ParameterizedString<"name">;
"exportRequested": string; "exportRequested": string;
"importRequested": string; "importRequested": string;
"lists": string; "lists": string;
@ -183,9 +190,9 @@ export interface Locale {
"wallpaper": string; "wallpaper": string;
"setWallpaper": string; "setWallpaper": string;
"removeWallpaper": string; "removeWallpaper": string;
"searchWith": string; "searchWith": ParameterizedString<"q">;
"youHaveNoLists": string; "youHaveNoLists": string;
"followConfirm": string; "followConfirm": ParameterizedString<"name">;
"proxyAccount": string; "proxyAccount": string;
"proxyAccountDescription": string; "proxyAccountDescription": string;
"host": string; "host": string;
@ -208,7 +215,7 @@ export interface Locale {
"software": string; "software": string;
"version": string; "version": string;
"metadata": string; "metadata": string;
"withNFiles": string; "withNFiles": ParameterizedString<"n">;
"monitor": string; "monitor": string;
"jobQueue": string; "jobQueue": string;
"cpuAndMemory": string; "cpuAndMemory": string;
@ -237,7 +244,7 @@ export interface Locale {
"processing": string; "processing": string;
"preview": string; "preview": string;
"default": string; "default": string;
"defaultValueIs": string; "defaultValueIs": ParameterizedString<"value">;
"noCustomEmojis": string; "noCustomEmojis": string;
"noJobs": string; "noJobs": string;
"federating": string; "federating": string;
@ -266,8 +273,8 @@ export interface Locale {
"imageUrl": string; "imageUrl": string;
"remove": string; "remove": string;
"removed": string; "removed": string;
"removeAreYouSure": string; "removeAreYouSure": ParameterizedString<"x">;
"deleteAreYouSure": string; "deleteAreYouSure": ParameterizedString<"x">;
"resetAreYouSure": string; "resetAreYouSure": string;
"areYouSure": string; "areYouSure": string;
"saved": string; "saved": string;
@ -285,8 +292,8 @@ export interface Locale {
"messageRead": string; "messageRead": string;
"noMoreHistory": string; "noMoreHistory": string;
"startMessaging": string; "startMessaging": string;
"nUsersRead": string; "nUsersRead": ParameterizedString<"n">;
"agreeTo": string; "agreeTo": ParameterizedString<"0">;
"agree": string; "agree": string;
"agreeBelow": string; "agreeBelow": string;
"basicNotesBeforeCreateAccount": string; "basicNotesBeforeCreateAccount": string;
@ -298,7 +305,7 @@ export interface Locale {
"images": string; "images": string;
"image": string; "image": string;
"birthday": string; "birthday": string;
"yearsOld": string; "yearsOld": ParameterizedString<"age">;
"registeredDate": string; "registeredDate": string;
"location": string; "location": string;
"theme": string; "theme": string;
@ -353,9 +360,9 @@ export interface Locale {
"thisYear": string; "thisYear": string;
"thisMonth": string; "thisMonth": string;
"today": string; "today": string;
"dayX": string; "dayX": ParameterizedString<"day">;
"monthX": string; "monthX": ParameterizedString<"month">;
"yearX": string; "yearX": ParameterizedString<"year">;
"pages": string; "pages": string;
"integration": string; "integration": string;
"connectService": string; "connectService": string;
@ -420,7 +427,7 @@ export interface Locale {
"recentlyUpdatedUsers": string; "recentlyUpdatedUsers": string;
"recentlyRegisteredUsers": string; "recentlyRegisteredUsers": string;
"recentlyDiscoveredUsers": string; "recentlyDiscoveredUsers": string;
"exploreUsersCount": string; "exploreUsersCount": ParameterizedString<"count">;
"exploreFediverse": string; "exploreFediverse": string;
"popularTags": string; "popularTags": string;
"userList": string; "userList": string;
@ -437,16 +444,16 @@ export interface Locale {
"moderationNote": string; "moderationNote": string;
"addModerationNote": string; "addModerationNote": string;
"moderationLogs": string; "moderationLogs": string;
"nUsersMentioned": string; "nUsersMentioned": ParameterizedString<"n">;
"securityKeyAndPasskey": string; "securityKeyAndPasskey": string;
"securityKey": string; "securityKey": string;
"lastUsed": string; "lastUsed": string;
"lastUsedAt": string; "lastUsedAt": ParameterizedString<"t">;
"unregister": string; "unregister": string;
"passwordLessLogin": string; "passwordLessLogin": string;
"passwordLessLoginDescription": string; "passwordLessLoginDescription": string;
"resetPassword": string; "resetPassword": string;
"newPasswordIs": string; "newPasswordIs": ParameterizedString<"password">;
"reduceUiAnimation": string; "reduceUiAnimation": string;
"share": string; "share": string;
"notFound": string; "notFound": string;
@ -466,7 +473,7 @@ export interface Locale {
"enable": string; "enable": string;
"next": string; "next": string;
"retype": string; "retype": string;
"noteOf": string; "noteOf": ParameterizedString<"user">;
"quoteAttached": string; "quoteAttached": string;
"quoteQuestion": string; "quoteQuestion": string;
"noMessagesYet": string; "noMessagesYet": string;
@ -486,12 +493,12 @@ export interface Locale {
"strongPassword": string; "strongPassword": string;
"passwordMatched": string; "passwordMatched": string;
"passwordNotMatched": string; "passwordNotMatched": string;
"signinWith": string; "signinWith": ParameterizedString<"x">;
"signinFailed": string; "signinFailed": string;
"or": string; "or": string;
"language": string; "language": string;
"uiLanguage": string; "uiLanguage": string;
"aboutX": string; "aboutX": ParameterizedString<"x">;
"emojiStyle": string; "emojiStyle": string;
"native": string; "native": string;
"disableDrawer": string; "disableDrawer": string;
@ -509,7 +516,7 @@ export interface Locale {
"regenerate": string; "regenerate": string;
"fontSize": string; "fontSize": string;
"mediaListWithOneImageAppearance": string; "mediaListWithOneImageAppearance": string;
"limitTo": string; "limitTo": ParameterizedString<"x">;
"noFollowRequests": string; "noFollowRequests": string;
"openImageInNewTab": string; "openImageInNewTab": string;
"dashboard": string; "dashboard": string;
@ -587,7 +594,7 @@ export interface Locale {
"deleteAllFiles": string; "deleteAllFiles": string;
"deleteAllFilesConfirm": string; "deleteAllFilesConfirm": string;
"removeAllFollowing": string; "removeAllFollowing": string;
"removeAllFollowingDescription": string; "removeAllFollowingDescription": ParameterizedString<"host">;
"userSuspended": string; "userSuspended": string;
"userSilenced": string; "userSilenced": string;
"yourAccountSuspendedTitle": string; "yourAccountSuspendedTitle": string;
@ -658,9 +665,9 @@ export interface Locale {
"wordMute": string; "wordMute": string;
"hardWordMute": string; "hardWordMute": string;
"regexpError": string; "regexpError": string;
"regexpErrorDescription": string; "regexpErrorDescription": ParameterizedString<"tab" | "line">;
"instanceMute": string; "instanceMute": string;
"userSaysSomething": string; "userSaysSomething": ParameterizedString<"name">;
"makeActive": string; "makeActive": string;
"display": string; "display": string;
"copy": string; "copy": string;
@ -686,7 +693,7 @@ export interface Locale {
"abuseReports": string; "abuseReports": string;
"reportAbuse": string; "reportAbuse": string;
"reportAbuseRenote": string; "reportAbuseRenote": string;
"reportAbuseOf": string; "reportAbuseOf": ParameterizedString<"name">;
"fillAbuseReportDescription": string; "fillAbuseReportDescription": string;
"abuseReported": string; "abuseReported": string;
"reporter": string; "reporter": string;
@ -701,7 +708,7 @@ export interface Locale {
"defaultNavigationBehaviour": string; "defaultNavigationBehaviour": string;
"editTheseSettingsMayBreakAccount": string; "editTheseSettingsMayBreakAccount": string;
"instanceTicker": string; "instanceTicker": string;
"waitingFor": string; "waitingFor": ParameterizedString<"x">;
"random": string; "random": string;
"system": string; "system": string;
"switchUi": string; "switchUi": string;
@ -711,10 +718,10 @@ export interface Locale {
"optional": string; "optional": string;
"createNewClip": string; "createNewClip": string;
"unclip": string; "unclip": string;
"confirmToUnclipAlreadyClippedNote": string; "confirmToUnclipAlreadyClippedNote": ParameterizedString<"name">;
"public": string; "public": string;
"private": string; "private": string;
"i18nInfo": string; "i18nInfo": ParameterizedString<"link">;
"manageAccessTokens": string; "manageAccessTokens": string;
"accountInfo": string; "accountInfo": string;
"notesCount": string; "notesCount": string;
@ -764,9 +771,9 @@ export interface Locale {
"needReloadToApply": string; "needReloadToApply": string;
"showTitlebar": string; "showTitlebar": string;
"clearCache": string; "clearCache": string;
"onlineUsersCount": string; "onlineUsersCount": ParameterizedString<"n">;
"nUsers": string; "nUsers": ParameterizedString<"n">;
"nNotes": string; "nNotes": ParameterizedString<"n">;
"sendErrorReports": string; "sendErrorReports": string;
"sendErrorReportsDescription": string; "sendErrorReportsDescription": string;
"myTheme": string; "myTheme": string;
@ -798,7 +805,7 @@ export interface Locale {
"publish": string; "publish": string;
"inChannelSearch": string; "inChannelSearch": string;
"useReactionPickerForContextMenu": string; "useReactionPickerForContextMenu": string;
"typingUsers": string; "typingUsers": ParameterizedString<"users">;
"jumpToSpecifiedDate": string; "jumpToSpecifiedDate": string;
"showingPastTimeline": string; "showingPastTimeline": string;
"clear": string; "clear": string;
@ -865,7 +872,7 @@ export interface Locale {
"misskeyUpdated": string; "misskeyUpdated": string;
"whatIsNew": string; "whatIsNew": string;
"translate": string; "translate": string;
"translatedFrom": string; "translatedFrom": ParameterizedString<"x">;
"accountDeletionInProgress": string; "accountDeletionInProgress": string;
"usernameInfo": string; "usernameInfo": string;
"aiChanMode": string; "aiChanMode": string;
@ -896,11 +903,11 @@ export interface Locale {
"continueThread": string; "continueThread": string;
"deleteAccountConfirm": string; "deleteAccountConfirm": string;
"incorrectPassword": string; "incorrectPassword": string;
"voteConfirm": string; "voteConfirm": ParameterizedString<"choice">;
"hide": string; "hide": string;
"useDrawerReactionPickerForMobile": string; "useDrawerReactionPickerForMobile": string;
"welcomeBackWithName": string; "welcomeBackWithName": ParameterizedString<"name">;
"clickToFinishEmailVerification": string; "clickToFinishEmailVerification": ParameterizedString<"ok">;
"overridedDeviceKind": string; "overridedDeviceKind": string;
"smartphone": string; "smartphone": string;
"tablet": string; "tablet": string;
@ -928,8 +935,8 @@ export interface Locale {
"cropYes": string; "cropYes": string;
"cropNo": string; "cropNo": string;
"file": string; "file": string;
"recentNHours": string; "recentNHours": ParameterizedString<"n">;
"recentNDays": string; "recentNDays": ParameterizedString<"n">;
"noEmailServerWarning": string; "noEmailServerWarning": string;
"thereIsUnresolvedAbuseReportWarning": string; "thereIsUnresolvedAbuseReportWarning": string;
"recommended": string; "recommended": string;
@ -938,7 +945,7 @@ export interface Locale {
"driveCapOverrideCaption": string; "driveCapOverrideCaption": string;
"requireAdminForView": string; "requireAdminForView": string;
"isSystemAccount": string; "isSystemAccount": string;
"typeToConfirm": string; "typeToConfirm": ParameterizedString<"x">;
"deleteAccount": string; "deleteAccount": string;
"document": string; "document": string;
"numberOfPageCache": string; "numberOfPageCache": string;
@ -992,7 +999,7 @@ export interface Locale {
"neverShow": string; "neverShow": string;
"remindMeLater": string; "remindMeLater": string;
"didYouLikeMisskey": string; "didYouLikeMisskey": string;
"pleaseDonate": string; "pleaseDonate": ParameterizedString<"host">;
"roles": string; "roles": string;
"role": string; "role": string;
"noRole": string; "noRole": string;
@ -1090,7 +1097,7 @@ export interface Locale {
"preservedUsernamesDescription": string; "preservedUsernamesDescription": string;
"createNoteFromTheFile": string; "createNoteFromTheFile": string;
"archive": string; "archive": string;
"channelArchiveConfirmTitle": string; "channelArchiveConfirmTitle": ParameterizedString<"name">;
"channelArchiveConfirmDescription": string; "channelArchiveConfirmDescription": string;
"thisChannelArchived": string; "thisChannelArchived": string;
"displayOfNote": string; "displayOfNote": string;
@ -1120,8 +1127,8 @@ export interface Locale {
"createCount": string; "createCount": string;
"inviteCodeCreated": string; "inviteCodeCreated": string;
"inviteLimitExceeded": string; "inviteLimitExceeded": string;
"createLimitRemaining": string; "createLimitRemaining": ParameterizedString<"limit">;
"inviteLimitResetCycle": string; "inviteLimitResetCycle": ParameterizedString<"time" | "limit">;
"expirationDate": string; "expirationDate": string;
"noExpirationDate": string; "noExpirationDate": string;
"inviteCodeUsedAt": string; "inviteCodeUsedAt": string;
@ -1134,7 +1141,7 @@ export interface Locale {
"expired": string; "expired": string;
"doYouAgree": string; "doYouAgree": string;
"beSureToReadThisAsItIsImportant": string; "beSureToReadThisAsItIsImportant": string;
"iHaveReadXCarefullyAndAgree": string; "iHaveReadXCarefullyAndAgree": ParameterizedString<"x">;
"dialog": string; "dialog": string;
"icon": string; "icon": string;
"forYou": string; "forYou": string;
@ -1189,7 +1196,7 @@ export interface Locale {
"doReaction": string; "doReaction": string;
"code": string; "code": string;
"reloadRequiredToApplySettings": string; "reloadRequiredToApplySettings": string;
"remainingN": string; "remainingN": ParameterizedString<"n">;
"overwriteContentConfirm": string; "overwriteContentConfirm": string;
"seasonalScreenEffect": string; "seasonalScreenEffect": string;
"decorate": string; "decorate": string;
@ -1202,7 +1209,7 @@ export interface Locale {
"replay": string; "replay": string;
"replaying": string; "replaying": string;
"ranking": string; "ranking": string;
"lastNDays": string; "lastNDays": ParameterizedString<"n">;
"backToTitle": string; "backToTitle": string;
"enableHorizontalSwipe": string; "enableHorizontalSwipe": string;
"_bubbleGame": { "_bubbleGame": {
@ -1221,7 +1228,7 @@ export interface Locale {
"end": string; "end": string;
"tooManyActiveAnnouncementDescription": string; "tooManyActiveAnnouncementDescription": string;
"readConfirmTitle": string; "readConfirmTitle": string;
"readConfirmText": string; "readConfirmText": ParameterizedString<"title">;
"shouldNotBeUsedToPresentPermanentInfo": string; "shouldNotBeUsedToPresentPermanentInfo": string;
"dialogAnnouncementUxWarn": string; "dialogAnnouncementUxWarn": string;
"silence": string; "silence": string;
@ -1236,10 +1243,10 @@ export interface Locale {
"theseSettingsCanEditLater": string; "theseSettingsCanEditLater": string;
"youCanEditMoreSettingsInSettingsPageLater": string; "youCanEditMoreSettingsInSettingsPageLater": string;
"followUsers": string; "followUsers": string;
"pushNotificationDescription": string; "pushNotificationDescription": ParameterizedString<"name">;
"initialAccountSettingCompleted": string; "initialAccountSettingCompleted": string;
"haveFun": string; "haveFun": ParameterizedString<"name">;
"youCanContinueTutorial": string; "youCanContinueTutorial": ParameterizedString<"name">;
"startTutorial": string; "startTutorial": string;
"skipAreYouSure": string; "skipAreYouSure": string;
"laterAreYouSure": string; "laterAreYouSure": string;
@ -1277,7 +1284,7 @@ export interface Locale {
"social": string; "social": string;
"global": string; "global": string;
"description2": string; "description2": string;
"description3": string; "description3": ParameterizedString<"link">;
}; };
"_postNote": { "_postNote": {
"title": string; "title": string;
@ -1315,7 +1322,7 @@ export interface Locale {
}; };
"_done": { "_done": {
"title": string; "title": string;
"description": string; "description": ParameterizedString<"link">;
}; };
}; };
"_timelineDescription": { "_timelineDescription": {
@ -1329,10 +1336,10 @@ export interface Locale {
}; };
"_serverSettings": { "_serverSettings": {
"iconUrl": string; "iconUrl": string;
"appIconDescription": string; "appIconDescription": ParameterizedString<"host">;
"appIconUsageExample": string; "appIconUsageExample": string;
"appIconStyleRecommendation": string; "appIconStyleRecommendation": string;
"appIconResolutionMustBe": string; "appIconResolutionMustBe": ParameterizedString<"resolution">;
"manifestJsonOverride": string; "manifestJsonOverride": string;
"shortName": string; "shortName": string;
"shortNameDescription": string; "shortNameDescription": string;
@ -1343,7 +1350,7 @@ export interface Locale {
"_accountMigration": { "_accountMigration": {
"moveFrom": string; "moveFrom": string;
"moveFromSub": string; "moveFromSub": string;
"moveFromLabel": string; "moveFromLabel": ParameterizedString<"n">;
"moveFromDescription": string; "moveFromDescription": string;
"moveTo": string; "moveTo": string;
"moveToLabel": string; "moveToLabel": string;
@ -1351,7 +1358,7 @@ export interface Locale {
"moveAccountDescription": string; "moveAccountDescription": string;
"moveAccountHowTo": string; "moveAccountHowTo": string;
"startMigration": string; "startMigration": string;
"migrationConfirm": string; "migrationConfirm": ParameterizedString<"account">;
"movedAndCannotBeUndone": string; "movedAndCannotBeUndone": string;
"postMigrationNote": string; "postMigrationNote": string;
"movedTo": string; "movedTo": string;
@ -1793,7 +1800,7 @@ export interface Locale {
"_signup": { "_signup": {
"almostThere": string; "almostThere": string;
"emailAddressInfo": string; "emailAddressInfo": string;
"emailSent": string; "emailSent": ParameterizedString<"email">;
}; };
"_accountDelete": { "_accountDelete": {
"accountDelete": string; "accountDelete": string;
@ -1846,14 +1853,14 @@ export interface Locale {
"save": string; "save": string;
"inputName": string; "inputName": string;
"cannotSave": string; "cannotSave": string;
"nameAlreadyExists": string; "nameAlreadyExists": ParameterizedString<"name">;
"applyConfirm": string; "applyConfirm": ParameterizedString<"name">;
"saveConfirm": string; "saveConfirm": ParameterizedString<"name">;
"deleteConfirm": string; "deleteConfirm": ParameterizedString<"name">;
"renameConfirm": string; "renameConfirm": ParameterizedString<"old" | "new">;
"noBackups": string; "noBackups": string;
"createdAt": string; "createdAt": ParameterizedString<"date" | "time">;
"updatedAt": string; "updatedAt": ParameterizedString<"date" | "time">;
"cannotLoad": string; "cannotLoad": string;
"invalidFile": string; "invalidFile": string;
}; };
@ -1898,8 +1905,8 @@ export interface Locale {
"featured": string; "featured": string;
"owned": string; "owned": string;
"following": string; "following": string;
"usersCount": string; "usersCount": ParameterizedString<"n">;
"notesCount": string; "notesCount": ParameterizedString<"n">;
"nameAndDescription": string; "nameAndDescription": string;
"nameOnly": string; "nameOnly": string;
"allowRenoteToExternal": string; "allowRenoteToExternal": string;
@ -1927,7 +1934,7 @@ export interface Locale {
"manage": string; "manage": string;
"code": string; "code": string;
"description": string; "description": string;
"installed": string; "installed": ParameterizedString<"name">;
"installedThemes": string; "installedThemes": string;
"builtinThemes": string; "builtinThemes": string;
"alreadyInstalled": string; "alreadyInstalled": string;
@ -1950,7 +1957,7 @@ export interface Locale {
"lighten": string; "lighten": string;
"inputConstantName": string; "inputConstantName": string;
"importInfo": string; "importInfo": string;
"deleteConstantConfirm": string; "deleteConstantConfirm": ParameterizedString<"const">;
"keys": { "keys": {
"accent": string; "accent": string;
"bg": string; "bg": string;
@ -2013,23 +2020,23 @@ export interface Locale {
"_ago": { "_ago": {
"future": string; "future": string;
"justNow": string; "justNow": string;
"secondsAgo": string; "secondsAgo": ParameterizedString<"n">;
"minutesAgo": string; "minutesAgo": ParameterizedString<"n">;
"hoursAgo": string; "hoursAgo": ParameterizedString<"n">;
"daysAgo": string; "daysAgo": ParameterizedString<"n">;
"weeksAgo": string; "weeksAgo": ParameterizedString<"n">;
"monthsAgo": string; "monthsAgo": ParameterizedString<"n">;
"yearsAgo": string; "yearsAgo": ParameterizedString<"n">;
"invalid": string; "invalid": string;
}; };
"_timeIn": { "_timeIn": {
"seconds": string; "seconds": ParameterizedString<"n">;
"minutes": string; "minutes": ParameterizedString<"n">;
"hours": string; "hours": ParameterizedString<"n">;
"days": string; "days": ParameterizedString<"n">;
"weeks": string; "weeks": ParameterizedString<"n">;
"months": string; "months": ParameterizedString<"n">;
"years": string; "years": ParameterizedString<"n">;
}; };
"_time": { "_time": {
"second": string; "second": string;
@ -2040,7 +2047,7 @@ export interface Locale {
"_2fa": { "_2fa": {
"alreadyRegistered": string; "alreadyRegistered": string;
"registerTOTP": string; "registerTOTP": string;
"step1": string; "step1": ParameterizedString<"a" | "b">;
"step2": string; "step2": string;
"step2Click": string; "step2Click": string;
"step2Uri": string; "step2Uri": string;
@ -2055,7 +2062,7 @@ export interface Locale {
"securityKeyName": string; "securityKeyName": string;
"tapSecurityKey": string; "tapSecurityKey": string;
"removeKey": string; "removeKey": string;
"removeKeyConfirm": string; "removeKeyConfirm": ParameterizedString<"name">;
"whyTOTPOnlyRenew": string; "whyTOTPOnlyRenew": string;
"renewTOTP": string; "renewTOTP": string;
"renewTOTPConfirm": string; "renewTOTPConfirm": string;
@ -2156,9 +2163,9 @@ export interface Locale {
}; };
"_auth": { "_auth": {
"shareAccessTitle": string; "shareAccessTitle": string;
"shareAccess": string; "shareAccess": ParameterizedString<"name">;
"shareAccessAsk": string; "shareAccessAsk": string;
"permission": string; "permission": ParameterizedString<"name">;
"permissionAsk": string; "permissionAsk": string;
"pleaseGoBack": string; "pleaseGoBack": string;
"callback": string; "callback": string;
@ -2217,12 +2224,12 @@ export interface Locale {
"_cw": { "_cw": {
"hide": string; "hide": string;
"show": string; "show": string;
"chars": string; "chars": ParameterizedString<"count">;
"files": string; "files": ParameterizedString<"count">;
}; };
"_poll": { "_poll": {
"noOnlyOneChoice": string; "noOnlyOneChoice": string;
"choiceN": string; "choiceN": ParameterizedString<"n">;
"noMore": string; "noMore": string;
"canMultipleVote": string; "canMultipleVote": string;
"expiration": string; "expiration": string;
@ -2232,16 +2239,16 @@ export interface Locale {
"deadlineDate": string; "deadlineDate": string;
"deadlineTime": string; "deadlineTime": string;
"duration": string; "duration": string;
"votesCount": string; "votesCount": ParameterizedString<"n">;
"totalVotes": string; "totalVotes": ParameterizedString<"n">;
"vote": string; "vote": string;
"showResult": string; "showResult": string;
"voted": string; "voted": string;
"closed": string; "closed": string;
"remainingDays": string; "remainingDays": ParameterizedString<"d" | "h">;
"remainingHours": string; "remainingHours": ParameterizedString<"h" | "m">;
"remainingMinutes": string; "remainingMinutes": ParameterizedString<"m" | "s">;
"remainingSeconds": string; "remainingSeconds": ParameterizedString<"s">;
}; };
"_visibility": { "_visibility": {
"public": string; "public": string;
@ -2281,7 +2288,7 @@ export interface Locale {
"changeAvatar": string; "changeAvatar": string;
"changeBanner": string; "changeBanner": string;
"verifiedLinkDescription": string; "verifiedLinkDescription": string;
"avatarDecorationMax": string; "avatarDecorationMax": ParameterizedString<"max">;
}; };
"_exportOrImport": { "_exportOrImport": {
"allNotes": string; "allNotes": string;
@ -2404,16 +2411,16 @@ export interface Locale {
}; };
"_notification": { "_notification": {
"fileUploaded": string; "fileUploaded": string;
"youGotMention": string; "youGotMention": ParameterizedString<"name">;
"youGotReply": string; "youGotReply": ParameterizedString<"name">;
"youGotQuote": string; "youGotQuote": ParameterizedString<"name">;
"youRenoted": string; "youRenoted": ParameterizedString<"name">;
"youWereFollowed": string; "youWereFollowed": string;
"youReceivedFollowRequest": string; "youReceivedFollowRequest": string;
"yourFollowRequestAccepted": string; "yourFollowRequestAccepted": string;
"pollEnded": string; "pollEnded": string;
"newNote": string; "newNote": string;
"unreadAntennaNote": string; "unreadAntennaNote": ParameterizedString<"name">;
"roleAssigned": string; "roleAssigned": string;
"emptyPushNotificationMessage": string; "emptyPushNotificationMessage": string;
"achievementEarned": string; "achievementEarned": string;
@ -2421,9 +2428,9 @@ export interface Locale {
"checkNotificationBehavior": string; "checkNotificationBehavior": string;
"sendTestNotification": string; "sendTestNotification": string;
"notificationWillBeDisplayedLikeThis": string; "notificationWillBeDisplayedLikeThis": string;
"reactedBySomeUsers": string; "reactedBySomeUsers": ParameterizedString<"n">;
"renotedBySomeUsers": string; "renotedBySomeUsers": ParameterizedString<"n">;
"followedBySomeUsers": string; "followedBySomeUsers": ParameterizedString<"n">;
"_types": { "_types": {
"all": string; "all": string;
"note": string; "note": string;
@ -2480,8 +2487,8 @@ export interface Locale {
}; };
}; };
"_dialog": { "_dialog": {
"charactersExceeded": string; "charactersExceeded": ParameterizedString<"current" | "max">;
"charactersBelow": string; "charactersBelow": ParameterizedString<"current" | "min">;
}; };
"_disabledTimeline": { "_disabledTimeline": {
"title": string; "title": string;

View File

@ -10,6 +10,7 @@ import { I18n } from '@/scripts/i18n.js';
export const i18n = markRaw(new I18n<Locale>(locale)); export const i18n = markRaw(new I18n<Locale>(locale));
export function updateI18n(newLocale) { export function updateI18n(newLocale: Locale) {
i18n.ts = newLocale; // @ts-expect-error -- private field
i18n.locale = newLocale;
} }

View File

@ -2,33 +2,114 @@
* SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ILocale, ParameterizedString } from '../../../../locales/index.js';
export class I18n<T extends Record<string, any>> { type FlattenKeys<T extends ILocale, TPrediction> = keyof {
public ts: T; [K in keyof T as T[K] extends ILocale
? FlattenKeys<T[K], TPrediction> extends infer C extends string
? `${K & string}.${C}`
: never
: T[K] extends TPrediction
? K
: never]: T[K];
};
constructor(locale: T) { type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString<string>>> = T extends ILocale
this.ts = locale; ? TKey extends `${infer K}.${infer C}`
// @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString<string>> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。
? ParametersOf<T[K], C>
: TKey extends keyof T
? T[TKey] extends ParameterizedString<infer P>
? P
: never
: never
: never;
type Ts<T extends ILocale> = {
readonly [K in keyof T as T[K] extends ParameterizedString<string> ? never : K]: T[K] extends ILocale ? Ts<T[K]> : string;
};
export class I18n<T extends ILocale> {
constructor(private locale: T) {
//#region BIND //#region BIND
this.t = this.t.bind(this); this.t = this.t.bind(this);
//#endregion //#endregion
} }
// string にしているのは、ドット区切りでのパス指定を許可するため public get ts(): Ts<T> {
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも if (_DEV_) {
public t(key: string, args?: Record<string, string | number>): string { class Handler<TTarget extends object> implements ProxyHandler<TTarget> {
try { get(target: TTarget, p: string | symbol): unknown {
let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; const value = target[p as keyof TTarget];
if (args) { if (typeof value === 'object') {
for (const [k, v] of Object.entries(args)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- 実際には null がくることはないので。
str = str.replace(`{${k}}`, v.toString()); return new Proxy(value!, new Handler<TTarget[keyof TTarget] & object>());
}
if (typeof value === 'string') {
const parameters = Array.from(value.matchAll(/\{(\w+)\}/g)).map(([, parameter]) => parameter);
if (parameters.length) {
console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`);
}
return value;
}
console.error(`Unexpected locale key: ${String(p)}`);
return p;
} }
} }
return str;
} catch (err) { return new Proxy(this.locale, new Handler()) as Ts<T>;
console.warn(`missing localization '${key}'`);
return key;
} }
return this.locale as Ts<T>;
}
/**
* @deprecated 使 locale vue
*/
public t<TKey extends FlattenKeys<T, string>>(key: TKey): string;
public t<TKey extends FlattenKeys<T, ParameterizedString<string>>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string;
public t(key: string, args?: { readonly [_: string]: string | number }) {
let str: string | ParameterizedString<string> | ILocale = this.locale;
for (const k of key.split('.')) {
str = str[k];
if (_DEV_) {
if (typeof str === 'undefined') {
console.error(`Unexpected locale key: ${key}`);
return key;
}
}
}
if (args) {
if (_DEV_) {
const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter));
if (missing.length) {
console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`);
}
}
for (const [k, v] of Object.entries(args)) {
const search = `{${k}}`;
if (_DEV_) {
if (!(str as string).includes(search)) {
console.error(`Unexpected locale parameter: ${k} at ${key}`);
}
}
str = (str as string).replace(search, v.toString());
}
}
return str;
} }
} }