From ce1dda2b8a7ad6f5b5efd08ddafb255992d4ecde Mon Sep 17 00:00:00 2001 From: dangered wolf Date: Tue, 30 Apr 2024 00:56:42 -0400 Subject: [PATCH] Localize new iv strings --- i18n/resources.json | 13 ++++--- package-lock.json | 75 +++++++++++++++++++++++++++++++++++++-- package.json | 3 +- src/constants.ts | 2 +- src/embed/status.ts | 3 +- src/render/instantview.ts | 66 +++++++++++++++++++--------------- src/render/video.ts | 1 - 7 files changed, 124 insertions(+), 39 deletions(-) diff --git a/i18n/resources.json b/i18n/resources.json index d5f1012..e896cbe 100644 --- a/i18n/resources.json +++ b/i18n/resources.json @@ -5,6 +5,9 @@ "quotedFrom": "Quoting {name} (@{screen_name})", "replyingTo": "Replying to @{screen_name}", "threadPartHeader": "A part of @${status.author.screen_name}'s thread", + "ivAuthorActionReply": "Reply from {authorName} (@{authorScreenName}):", + "ivAuthorActionOriginal": "Original from {authorName} (@{authorScreenName}):", + "ivAuthorActionFollowUp": "Follow-up from {authorName} (@{authorScreenName}):", "photoCount": "Photo {number} / {total}", "videoCount": "Video {number} / {total}", @@ -15,19 +18,21 @@ "ivOriginalText": "Original text", "ivViewOriginal": "View full thread", - "ivViewOriginalStatus": "View full thread", "ivAboutAuthor": "About author", - "ivProfileFollowing": "Following", - "ivProfileFollowers": "{numFollowers, plural, one {# Follower} other {# Followers}}", - "ivProfileStatuses": "{numPosts, plural, one {# Post} other {# Posts}}", + "ivProfileFollowing": "{numFollowing, plural, one {Following} other {Following}}", + "ivProfileFollowers": "{numFollowers, plural, one {Follower} other {Followers}}", + "ivProfileStatuses": "{numPosts, plural, one {Post} other {Posts}}", + "ivProfilePictureAlt": "{author}'s profile picture", "ivFallbackText": "If you can see this, your browser is doing something weird with your user agent.", "ivInternetArchiveText": "{brandingName} archive", "pollFinalResults": "Final results", "pollVotes": "{voteCount, plural, one {# vote} other {# votes}} ยท {timeLeft}", + "ivPollChoice": "{voteCount, plural, one {# vote} other {# votes}}, {percentage}%", "ivQuoteHeader": "Quoting {authorName} (@{authorHandle})", + "ivCommunityNoteHeader": "Readers added context they thought people might want to know", "language_af": "Afrikaans", "language_ar": "Arabic", diff --git a/package-lock.json b/package-lock.json index 8de3cc5..404b1c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@hono/sentry": "^1.0.1", "hono": "^3.12.12", - "i18next": "^23.8.2" + "i18next": "^23.8.2", + "i18next-icu": "^2.3.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240423.0", @@ -769,6 +770,55 @@ "node": ">=14" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", + "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", + "peer": true, + "dependencies": { + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.6.tgz", + "integrity": "sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA==", + "peer": true, + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/icu-skeleton-parser": "1.8.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.0.tgz", + "integrity": "sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA==", + "peer": true, + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@hono/sentry": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@hono/sentry/-/sentry-1.0.1.tgz", @@ -4568,6 +4618,14 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/i18next-icu": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.3.0.tgz", + "integrity": "sha512-x+j7kd5nDJCfbU53uwsMfXD7ALPu5uv0bqjAMQ5nVvXRoj1L7gkmswKtM3XDWYo4YUHf1jznlhSdPyy0xEwU+Q==", + "peerDependencies": { + "intl-messageformat": "^10.3.3" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -4642,6 +4700,18 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/intl-messageformat": { + "version": "10.5.11", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.11.tgz", + "integrity": "sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==", + "peer": true, + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.6", + "tslib": "^2.4.0" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8647,8 +8717,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/type-check": { "version": "0.4.0", diff --git a/package.json b/package.json index 9a7db4d..f987eb8 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ }, "dependencies": { "@hono/sentry": "^1.0.1", + "hono": "^3.12.12", "i18next": "^23.8.2", - "hono": "^3.12.12" + "i18next-icu": "^2.3.0" } } diff --git a/src/constants.ts b/src/constants.ts index 2426cab..c5cd8ad 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,7 +15,7 @@ export const Constants = { REDIRECT_URL: REDIRECT_URL, RELEASE_NAME: RELEASE_NAME, GIF_TRANSCODE_DOMAIN: GIF_TRANSCODE_DOMAIN, - API_DOCS_URL: `https://github.com/dangeredwolf/FixTweet/wiki/API-Home`, + API_DOCS_URL: `https://github.com/FixTweet/FxTwitter/wiki/API-Home`, TWITTER_ROOT: 'https://twitter.com', TWITTER_GLOBAL_NAME_ROOT: 'twitter.com', TWITTER_API_ROOT: 'https://api.twitter.com', diff --git a/src/embed/status.ts b/src/embed/status.ts index 58b537d..cfe2e8a 100644 --- a/src/embed/status.ts +++ b/src/embed/status.ts @@ -10,6 +10,7 @@ import { renderInstantView } from '../render/instantview'; import { constructTwitterThread } from '../providers/twitter/conversation'; import { Experiment, experimentCheck } from '../experiments'; import i18next from 'i18next'; +import icu from "i18next-icu"; import translationResources from '../../i18n/resources.json'; export const returnError = (c: Context, error: string): Response => { @@ -128,7 +129,7 @@ export const handleStatus = async ( let overrideMedia: APIMedia | undefined; - await i18next.init({ + await i18next.use(icu).init({ lng: language ?? status.lang ?? 'en', debug: true, resources: translationResources, diff --git a/src/render/instantview.ts b/src/render/instantview.ts index 37bf87f..2acb387 100644 --- a/src/render/instantview.ts +++ b/src/render/instantview.ts @@ -10,7 +10,7 @@ enum AuthorActionType { FollowUp = 'FollowUp' } -const populateUserLinks = (status: APIStatus, text: string): string => { +const populateUserLinks = (text: string): string => { /* TODO: Maybe we can add username splices to our API so only genuinely valid users are linked? */ text.match(/@(\w{1,15})/g)?.forEach(match => { const username = match.replace('@', ''); @@ -22,7 +22,7 @@ const populateUserLinks = (status: APIStatus, text: string): string => { return text; }; -const generateStatusMedia = (status: APIStatus, author: APIUser): string => { +const generateStatusMedia = (status: APIStatus): string => { let media = ''; if (status.media?.all?.length) { status.media.all.forEach(mediaItem => { @@ -95,7 +95,7 @@ function getTranslatedText(status: APITwitterStatus, isQuote = false): string | let text = paragraphify(sanitizeText(status.translation?.text), isQuote); text = htmlifyLinks(text); text = htmlifyHashtags(text); - text = populateUserLinks(status, text); + text = populateUserLinks(text); const formatText = `๐Ÿ“‘ {translation}`.format({ translation: i18next.t('translatedFrom').format({ @@ -124,16 +124,28 @@ const generateInlineAuthorHeader = ( author: APIUser, authorActionType: AuthorActionType | null ): string => { - return `

{AuthorAction} from ${author.name} (@${author.screen_name}):

`.format( - { - AuthorAction: - authorActionType === AuthorActionType.Reply - ? 'Reply' - : authorActionType === AuthorActionType.Original - ? 'Original' - : 'Follow-up' - } - ); + if (authorActionType === AuthorActionType.Original) { + return `

${i18next.t('ivAuthorActionOriginal', { + statusUrl: status.url, + authorName: author.name, + authorUrl: author.url, + authorScreenName: author.screen_name + })}

`; + } else if (authorActionType === AuthorActionType.FollowUp) { + return `

${i18next.t('ivAuthorActionFollowUp', { + statusUrl: status.url, + authorName: author.name, + authorUrl: author.url, + authorScreenName: author.screen_name + })}

`; + } + // Reply / unknown + return `

${i18next.t('ivAuthorActionReply', { + statusUrl: status.url, + authorName: author.name, + authorUrl: author.url, + authorScreenName: author.screen_name + })}

`; }; const wrapForeignLinks = (url: string) => { @@ -158,7 +170,7 @@ const generateStatusFooter = (status: APIStatus, isQuote = false, author: APIUse let description = author.description; description = htmlifyLinks(description); description = htmlifyHashtags(description); - description = populateUserLinks(status, description); + description = populateUserLinks(description); return `

{socialText}

@@ -168,7 +180,7 @@ const generateStatusFooter = (status: APIStatus, isQuote = false, author: APIUse `.format({ socialText: getSocialTextIV(status as APITwitterStatus) || '', viewOriginal: !isQuote - ? `${i18next.t('ivViewOriginalStatus')}` + ? `${i18next.t('ivViewOriginal')}` : notApplicableComment, aboutSection: isQuote ? '' @@ -179,13 +191,11 @@ const generateStatusFooter = (status: APIStatus, isQuote = false, author: APIUse

${description}

{location} {website} {joined}

- {following} ${i18next.t('ivProfileFollowing')}โ€‚ - {followers} ${i18next.t('ivProfileFollowers')}โ€‚ - {statuses} ${i18next.t('ivProfileStatuses')} + {following} ${i18next.t('ivProfileFollowing', { numFollowing: author.following })}โ€‚ + {followers} ${i18next.t('ivProfileFollowers', { numFollowers: author.followers })}โ€‚ + {statuses} ${i18next.t('ivProfileStatuses', { numStatuses: author.statuses })}

`.format({ - pfp: `${
-            author.name
-          }'s profile picture`, + pfp: `${i18next.t('ivProfilePictureAlt', { author: author.name })}`, location: author.location ? `๐Ÿ“Œ ${author.location}` : '', website: author.website ? `๐Ÿ”— ${author.website.display_url}` @@ -207,10 +217,10 @@ const generatePoll = (poll: APIPoll, language: string): string => { poll.choices.forEach(choice => { const bar = 'โ–ˆ'.repeat((choice.percentage / 100) * barLength); // eslint-disable-next-line no-irregular-whitespace - str += `${bar}
${choice.label}
${intlFormat.format(choice.count)} votes, ${intlFormat.format(choice.percentage)}%
`; + str += `${bar}
${choice.label}
${i18next.t('ivPollChoice', { voteCount: intlFormat.format(choice.count), percentage: intlFormat.format(choice.percentage)})}
`; }); /* Finally, add the footer of the poll with # of votes and time left */ - str += `
${intlFormat.format(poll.total_votes)} votes ยท ${poll.time_left_en}`; + str += `
${i18next.t('pollVotes', { voteCount: intlFormat.format(poll.total_votes), timeLeft: poll.time_left_en})}`; return str; }; @@ -245,7 +255,7 @@ const generateCommunityNote = (status: APITwitterStatus): string => { // Add the remaining text after the last link result = ` - + @@ -273,7 +283,7 @@ const generateStatus = ( return ` {quoteHeader} - ${generateStatusMedia(status, author)} + ${generateStatusMedia(status)} ${translatedText ? translatedText : notApplicableComment} @@ -286,7 +296,7 @@ const generateStatus = ( ${status.poll ? generatePoll(status.poll, status.lang ?? 'en') : notApplicableComment} ${!isQuote && status.quote ? generateStatus(status.quote, author, true, null) : notApplicableComment} -
${!isQuote ? `${i18next.t('ivViewOriginalStatus')}` : notApplicableComment} +
${!isQuote ? `${i18next.t('ivViewOriginal')}` : notApplicableComment} `.format({ quoteHeader: isQuote ? '

' + @@ -337,7 +347,7 @@ export const renderInstantView = (properties: RenderProperties): ResponseInstruc flags?.archive ? i18next.t('ivInternetArchiveText').format({ brandingName: Constants.BRANDING_NAME }) : i18next.t('ivFallbackText') - } ${i18next.t('ivViewOriginalStatus')} + } ${i18next.t('ivViewOriginal')} `; return instructions; diff --git a/src/render/video.ts b/src/render/video.ts index 19b4588..73c0c96 100644 --- a/src/render/video.ts +++ b/src/render/video.ts @@ -2,7 +2,6 @@ import i18next from 'i18next'; import { Constants } from '../constants'; import { Experiment, experimentCheck } from '../experiments'; import { handleQuote } from '../helpers/quote'; -import { Strings } from '../strings'; export const renderVideo = ( properties: RenderProperties,

Readers added context they thought people might want to know${i18next.t('ivCommunityNoteHeader')}
${result.replace(/\n/g, '\n
')}