From e97c5c9ea5d0ccdb1fac90943b0178a6aa78d0d9 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 22 Feb 2023 17:36:30 -0800 Subject: [PATCH 01/15] test: test i18n languages and fallbacks --- package-lock.json | 14 ++++----- package.json | 2 +- src/i18n.test.js | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 src/i18n.test.js diff --git a/package-lock.json b/package-lock.json index 1a55b6ffa..06a9bd5b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,7 +109,7 @@ "@svgr/cli": "^5.4.0", "@types/esm": "^3.2.0", "@types/jest": "^29.4.0", - "@types/node": "^14.0.27", + "@types/node": "^14.18.36", "@types/path-browserify": "^1.0.0", "@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/parser": "^5.30.7", @@ -18132,9 +18132,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "14.14.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz", - "integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==" + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==" }, "node_modules/@types/node-fetch": { "version": "2.6.2", @@ -83792,9 +83792,9 @@ "dev": true }, "@types/node": { - "version": "14.14.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz", - "integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==" + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==" }, "@types/node-fetch": { "version": "2.6.2", diff --git a/package.json b/package.json index ea30ee23a..d87a63f70 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "@svgr/cli": "^5.4.0", "@types/esm": "^3.2.0", "@types/jest": "^29.4.0", - "@types/node": "^14.0.27", + "@types/node": "^14.18.36", "@types/path-browserify": "^1.0.0", "@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/parser": "^5.30.7", diff --git a/src/i18n.test.js b/src/i18n.test.js new file mode 100644 index 000000000..60937b9a5 --- /dev/null +++ b/src/i18n.test.js @@ -0,0 +1,76 @@ +/* global describe, it, expect, beforeAll, afterAll */ +// @ts-check +import { createServer } from 'http-server' +import i18n from './i18n.js' +import { readdir } from 'node:fs/promises' + +const allLanguages = (await readdir('./public/locales', { withFileTypes: true })) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name) + +describe('i18n', function () { + /** + * @type {import('http-server').HTTPServer} + */ + let httpServer + beforeAll(async function () { + httpServer = createServer({ + root: './public' + }) + await httpServer.listen(80) + + // initialize i18n + await i18n.init() + }) + + afterAll(async function () { + await httpServer.close() + }) + + it('should have a default language', function () { + expect(i18n.language).toBe('en-US') + }) + + allLanguages.concat('ko').forEach((lang) => { + describe(`lang=${lang}`, function () { + it(`should be able to switch to ${lang}`, async function () { + await i18n.changeLanguage(lang) + + expect(i18n.language).toBe(lang) + }) + + it(`should have a key for ${lang}`, async function () { + // key and namespace that don't exist return the key without the leading namespace + expect(await i18n.t('someNs:that.doesnt.exist', { lng: lang })).toBe('that.doesnt.exist') + // missing key on existing namespace returns that key + expect(await i18n.t('app:that.doesnt.exist', { lng: lang })).toBe('that.doesnt.exist') + expect(await i18n.t('app:actions.add', { lng: lang })).not.toBe('app:actions.add') + }) + }) + }) + + describe('fallback languages', function () { + /** + * @type {import('i18next').FallbackLngObjList} + */ + const fallbackLanguages = /** @type {import('i18next').FallbackLngObjList} */(i18n.options.fallbackLng) + for (const lng in fallbackLanguages) { + if (lng === 'default') { + continue + } + const fallbackArr = fallbackLanguages[lng] + fallbackArr.forEach((fallbackLang) => { + it(`fallback '${fallbackLang}' (for '${lng}') is valid`, async function () { + expect(allLanguages).toContain(fallbackLang) + }) + }) + it(`language ${lng} should fallback to ${fallbackArr[0]}`, async function () { + const result = await i18n.t('app:actions.add', { lng }) + const englishResult = await i18n.t('app:actions.add', { lng: 'en' }) + const fallbackResult = await i18n.t('app:actions.add', { lng: fallbackArr[0] }) + expect(result).toBe(fallbackResult) + expect(result).not.toBe(englishResult) + }) + } + }) +}) From 87b68e674dc86d82b62a1cbb45f7039127a20d38 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:34:16 -0800 Subject: [PATCH 02/15] test(i18n): use available port --- src/i18n.test.js | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/i18n.test.js b/src/i18n.test.js index 60937b9a5..833ae2fb2 100644 --- a/src/i18n.test.js +++ b/src/i18n.test.js @@ -3,32 +3,50 @@ import { createServer } from 'http-server' import i18n from './i18n.js' import { readdir } from 'node:fs/promises' +import getPort from 'get-port' + +const backendListenerPort = await getPort({ port: getPort.makeRange(3000, 4000) }) const allLanguages = (await readdir('./public/locales', { withFileTypes: true })) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name) -describe('i18n', function () { - /** +/** * @type {import('http-server').HTTPServer} */ - let httpServer - beforeAll(async function () { - httpServer = createServer({ - root: './public' - }) - await httpServer.listen(80) - - // initialize i18n - await i18n.init() +let httpServer +beforeAll(async function () { + httpServer = createServer({ + root: './public', + cors: true }) + await httpServer.listen(backendListenerPort) - afterAll(async function () { - await httpServer.close() + // initialize i18n + await i18n.init({ + backend: { + ...i18n.options?.backend, + backendOptions: [ + i18n.options?.backend?.backendOptions?.[0], + { + loadPath: `http://localhost:${backendListenerPort}/locales/{{lng}}/{{ns}}.json` + } + ] + } }) +}) +afterAll(async function () { + await httpServer.close() +}) +describe('i18n', function () { it('should have a default language', function () { expect(i18n.language).toBe('en-US') + expect(i18n.isInitialized).toBe(true) + }) + + it('should return key for non-existent language', function () { + expect(i18n.t('app:actions.add', { lng: 'xx' })).toBe('actions.add') }) allLanguages.concat('ko').forEach((lang) => { @@ -44,7 +62,8 @@ describe('i18n', function () { expect(await i18n.t('someNs:that.doesnt.exist', { lng: lang })).toBe('that.doesnt.exist') // missing key on existing namespace returns that key expect(await i18n.t('app:that.doesnt.exist', { lng: lang })).toBe('that.doesnt.exist') - expect(await i18n.t('app:actions.add', { lng: lang })).not.toBe('app:actions.add') + const langResult = await i18n.t('app:actions.add', { lng: lang }) + expect(langResult).not.toBe('actions.add') }) }) }) From d1cac49c15dfffc88cc3450c069270d9d2a8960c Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:45:36 -0800 Subject: [PATCH 03/15] chore: add all local files for ko-KR english has 8 locale files: `ls -lhatr public/locales/en/*.json | wc -l # 8` before this change, ko-KR only had 4 `ls -lhatr public/locales/ko-KR/*.json | wc -l # 4` so I copied them over using `cp -n public/locales/en/*.json public/locales/ko-KR/` after this change `ls -lhatr public/locales/ko-KR/*.json | wc -l # 8` --- public/locales/ko-KR/app.json | 106 +++++++++++++++++ public/locales/ko-KR/notify.json | 30 +++++ public/locales/ko-KR/settings.json | 184 +++++++++++++++++++++++++++++ public/locales/ko-KR/welcome.json | 38 ++++++ 4 files changed, 358 insertions(+) create mode 100644 public/locales/ko-KR/app.json create mode 100644 public/locales/ko-KR/notify.json create mode 100644 public/locales/ko-KR/settings.json create mode 100644 public/locales/ko-KR/welcome.json diff --git a/public/locales/ko-KR/app.json b/public/locales/ko-KR/app.json new file mode 100644 index 000000000..142c477b2 --- /dev/null +++ b/public/locales/ko-KR/app.json @@ -0,0 +1,106 @@ +{ + "actions": { + "add": "Add", + "apply": "Apply", + "browse": "Browse", + "cancel": "Cancel", + "change": "Change", + "clear": "Clear", + "close": "Close", + "copy": "Copy", + "create": "Create", + "remove": "Remove", + "download": "Download", + "edit": "Edit", + "import": "Import", + "inspect": "Inspect", + "more": "More", + "moreInfo": "More info", + "noThanks": "No thanks", + "ok": "OK", + "pinVerb": "Pin", + "rename": "Rename", + "reset": "Reset", + "save": "Save", + "saving": "Saving…", + "selectAll": "Select all", + "setPinning": "Set pinning", + "submit": "Submit", + "unpin": "Unpin", + "unselectAll": "Unselect all", + "generate": "Generate", + "publish": "Publish", + "downloadCar": "Download as CAR", + "done": "Done" + }, + "cliModal": { + "description": "Paste the following into your terminal to do this task in IPFS via the command line. Remember that you'll need to replace placeholders with your specific parameters." + }, + "nav": { + "bugsLink": "Report a bug", + "codeLink": "See the code" + }, + "status": { + "connectedToIpfs": "Connected to IPFS", + "connectingToIpfs": "Connecting to IPFS…", + "couldNotConnect": "Could not connect to the IPFS API" + }, + "apiAddressForm": { + "placeholder": "Enter a URL (http://127.0.0.1:5001) or a Multiaddr (/ip4/127.0.0.1/tcp/5001)" + }, + "publicGatewayForm": { + "placeholder": "Enter a URL (https://dweb.link)" + }, + "terms": { + "address": "Address", + "addresses": "Addresses", + "advanced": "Advanced", + "agent": "Agent", + "api": "API", + "apiAddress": "API address", + "blocks": "Blocks", + "connection": "Connection", + "downSpeed": "Incoming", + "example": "Example:", + "file": "File", + "files": "Files", + "folder": "Folder", + "folders": "Folders", + "gateway": "Gateway", + "in": "In", + "latency": "Latency", + "loading": "Loading", + "location": "Location", + "name": "Name", + "node": "Node", + "out": "Out", + "peer": "Peer", + "peerId": "Peer ID", + "id": "ID", + "peers": "Peers", + "pinNoun": "Pin", + "pins": "Pins", + "pinStatus": "Pin Status", + "publicKey": "Public key", + "publicGateway": "Public Gateway", + "rateIn": "Rate in", + "rateOut": "Rate out", + "repo": "Repo", + "size": "Size", + "totalIn": "Total in", + "totalOut": "Total out", + "unknown": "Unknown", + "ui": "UI", + "upSpeed": "Outgoing", + "revision": "Revision" + }, + "tour": { + "back": "Back", + "close": "Close", + "finish": "Finish", + "next": "Next", + "skip": "Skip", + "tooltip": "Click this button any time for a guided tour on the current page." + }, + "startTourHelper": "Start tour" +} diff --git a/public/locales/ko-KR/notify.json b/public/locales/ko-KR/notify.json new file mode 100644 index 000000000..774ede1fd --- /dev/null +++ b/public/locales/ko-KR/notify.json @@ -0,0 +1,30 @@ +{ + "ipfsApiRequestFailed": "Could not connect. Please check if your daemon is running.", + "windowIpfsRequestFailed": "IPFS request failed. Please check your IPFS Companion settings.", + "ipfsInvalidApiAddress": "The provided IPFS API address is invalid.", + "ipfsConnectSuccess": "Successfully connected to the IPFS API address", + "ipfsConnectFail": "Unable to connect to the provided IPFS API address", + "ipfsPinFailReason": "Unable to set pinning at {serviceName}: {errorMsg}", + "ipfsPinSucceedReason": "Successfully pinned at {serviceName}", + "ipfsUnpinSucceedReason": "Successfully unpinned from {serviceName}", + "ipfsIsBack": "Normal IPFS service has resumed. Enjoy!", + "folderExists": "An item with that name already exists. Please choose another.", + "filesFetchFailed": "Failed to get those files. Please check the path and try again.", + "filesRenameFailed": "Failed to rename the files. Please try again.", + "filesMakeDirFailed": "Failed to create directory. Please try again.", + "filesCopyFailed": "Failed to copy the files. Please try again.", + "filesRemoveFailed": "Failed to remove the files. Please try again.", + "filesAddFailed": "Failed to add files to IPFS. Please try again.", + "filesEventFailed": "Failed to process your request. Please try again.", + "couldntConnectToPeer": "Could not connect to the provided peer.", + "connectedToPeer": "Successfully connected to the provided peer.", + "experimentsErrors": { + "npmOnIpfs": "We couldn't make changes to your system. Please install or uninstall 'ipfs-npm' package manually." + }, + "desktopToggleErrors": { + "autoLaunch": "Could not toggle launch at startup option.", + "ipfsOnPath": "Could not toggle IPFS command line tools.", + "downloadHashShortcut": "Could not toggle download hash shortcut.", + "screenshotShortcut": "Could not toggle screenshot shortcut." + } +} diff --git a/public/locales/ko-KR/settings.json b/public/locales/ko-KR/settings.json new file mode 100644 index 000000000..41f1245e9 --- /dev/null +++ b/public/locales/ko-KR/settings.json @@ -0,0 +1,184 @@ +{ + "title": "Settings", + "save": "Save", + "saving": "Saving…", + "reset": "Reset", + "learnMoreLink": "Learn more.", + "showOptions": "Show options", + "pinningServices": { + "title": "Pinning Services", + "description": "Use local pinning when you want to ensure an item on your node is never garbage-collected, even if you remove it from Files. You can also link your accounts with other remote pinning services to automatically or selectively persist files with those providers, enabling you to keep backup copies of your files and/or make them available to others when your local node is offline.", + "noPinRemoteDescription": "Support for third-party remote pinning services is missing. Update your IPFS node to Kubo 0.8.0 or later, and you'll be able to add and configure them here.", + "addAutoUpload": "Enable auto upload", + "removeAutoUpload": "Disable Auto Upload" + }, + "language": "Language", + "analytics": "Analytics", + "cliTutorMode": "CLI Tutor Mode", + "config": "IPFS Config", + "languageModal": { + "title": "Change language", + "description": "Pick your preferred language.", + "translationProjectIntro": "Add or improve translations and make IPFS better for everyone!", + "translationProjectLink": "Join the IPFS Translation Project" + }, + "apiDescription": "<0>If your node is configured with a <1>custom API address, including a port other than the default 5001, enter it here.", + "publicGatewayDescription": "<0>Choose which <1>public gateway you want to use when generating shareable links.", + "cliDescription": "<0>Enable this option to display a \"view code\" <1> icon next to common IPFS commands. Clicking it opens a modal with that command's CLI code, so you can paste it into the IPFS command-line interface in your terminal.", + "cliModal": { + "extraNotesJsonConfig": "If you've made changes to the config in this page's code editor that you'd like to save, click the download icon next to the copy button to download it as a JSON file." + }, + "pinningModal": { + "title": "Select a pinning service provider.", + "description": "Don’t see your pinning service provider? <1>Add a custom one.<1>" + }, + "pinningServiceModal": { + "title": "Configure a custom pinning service.", + "description": "Want to make your custom pinning service available to others? <1>Learn how.<1>", + "service": "Service", + "nickname": "Nickname", + "nicknamePlaceholder": "Name for your service", + "apiEndpoint": "API endpoint", + "apiEndpointPlaceholder": "URL for its API endpoint", + "secretApiKey": "Secret access token", + "secretApiKeyHowToLink": "How to generate a new token", + "autoUpload": "Auto upload", + "complianceLabel": "🔍 {nickname} pinning compliance report" + }, + "autoUploadModal": { + "title": "Change upload policy for {name}", + "description": "When enabled, auto upload will periodically pin the root of your Files directory, ensuring everything is pinned remotely and available to other peers even when your local IPFS node goes offline. If you disable it, you can still pin each file manually using the \"Set pinning\" option on the Files screen." + }, + "errors": { + "nickname": "Nickname is required", + "apiError": "API error", + "apiEndpoint": "Must be a valid URL", + "secretApiKey": "Secret access token is required", + "failedToFetch": "Failed to fetch", + "failedToFetchTitle": "Unable to fetch pin count from this remote service. Make sure it is online and that you entered correct credentials. If this is a newly added service, try removing and adding it again." + }, + "actions": { + "addService": "Add Service", + "generateKey": "Generate Key", + "edit": "Change", + "close": "Close", + "save": "Save", + "cancel": "Cancel", + "enable": "Enable", + "disable": "Disable" + }, + "edit": "Edit", + "visitService": "Visit service", + "remove": "Remove", + "localPinning": "Local Pinning", + "service": "Service", + "size": "Size", + "pins": "Pins", + "autoUpload": "Auto Upload", + "autoUploadPolicy": { + "true": "All files", + "false": "Off" + }, + "fetchingSettings": "Fetching settings...", + "configApiNotAvailable": "The IPFS config API is not available. Please disable the \"IPFS Companion\" Web Extension and try again.", + "ipfsDaemonOffline": "The IPFS daemon is offline. Please turn it on and try again.", + "settingsUnavailable": "Settings not available. Please check your IPFS daemon is running.", + "settingsHaveChanged": "The settings have changed; please click <1>Reset to update the editor contents.", + "errorOccured": "An error occured while saving your changes", + "checkConsole": "Check the browser console for more info.", + "changesSaved": "Your changes have been saved.", + "settingsWillBeUsedNextTime": "The new settings will be used next time you restart the IPFS daemon.", + "ipfsConfigDescription": "The IPFS config file is a JSON document. It is read once when the IPFS daemon is started. Save your changes, then restart the IPFS daemon to apply them.", + "ipfsConfigHelp": "Check the documentation for further information.", + "AnalyticsToggle": { + "label": "Help improve this app by sending anonymous usage data", + "summary": "Configure what is collected", + "paragraph1": "No CIDs, filenames, or other personal information are collected. We want metrics to show us which features are useful to help us prioritise what to work on next, and system configuration information to guide our testing.", + "paragraph2": "Protocol Labs hosts a <1>Countly instance to record anonymous usage data for this app.", + "basicInfo": "The following additional information is sent to countly.ipfs.io", + "optionalInfo": "You can opt-in to send the following information:", + "sessions": { + "label": "Sessions", + "summary": "When and how long you use the app, and browser metrics", + "details": "<0>The following browser metrics are sent:<1><0>A random, generated device ID<1>Timestamp when the session starts<2>Periodic timestamps to track duration<3>App version; e.g. 2.4.4<4>Locale; e.g. en-GB<5>User agent; e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) …<6>Screen resolution; e.g. 800×600<7>Screen pixel density; e.g. 1" + }, + "events": { + "label": "Events", + "summary": "actions like adding or deleting files, and how long it took", + "details": "App-specific actions. We record only that the action happened, how long it took from start to finish, and a count if the event involved multiple items." + }, + "views": { + "label": "Page views", + "summary": "Which sections of the app you visit", + "details": "Records which sections of the apps are visited. The paths recorded only include the pattern used to match the route rather than the full url." + }, + "location": { + "label": "Location", + "summary": "Country code from IP address (IP address is discarded)", + "details": "Your IP address is used to calculate a country code for your location, like \"FR\" for France. Where consent is not given to track location, an empty location property is sent with all requests to signal to the server to disable the location look up. This information helps guide our translation effort and figure out where we should put on events." + }, + "crashes": { + "label": "App errors", + "summary": "JavaScript error messages and stack traces", + "details": "Records JavaScript error messages and stack traces that occur while using the app, where possible. It is very helpful to know when the app is not working for you, but <1>error messages may include identifiable information like CIDs or file paths, so only enable this if you are comfortable sharing that information with us." + } + }, + "cliToggle": { + "label": "Enable command-line interface (CLI) tutor mode" + }, + "tour": { + "step1": { + "title": "Settings page", + "paragraph1": "Here you can change the settings of your Web UI and IPFS node.", + "paragraph2": "If you're running IPFS Desktop you'll have some specific settings for it too." + }, + "step2": { + "title": "Custom API address", + "paragraph1": "If you've configured your IPFS node with a custom API address, you can update your config file right here instead of editing the JSON by hand." + }, + "step3": { + "title": "Pinning services", + "paragraph1": "<0>If you have accounts with third-party remote pinning services, add them here so you can pin/unpin items to those services directly from the Files screen. You can learn more about third-party pinning services in the <1>IPFS Docs." + }, + "step4": { + "title": "Language selector", + "paragraph1": "<0>You can change the language of the Web UI. If your preferred language isn't available, head over our project page in <1>Transifex to help us translate!" + }, + "step5": { + "title": "Anonymous usage analytics", + "paragraph1": "If you opt-in, you can help us make the Web UI better by sending anonymous usage analytics.", + "paragraph2": "You're able to choose what data you send us and we won't be able to identify you, we value privacy above all else." + }, + "step6": { + "title": "CLI tutor mode", + "paragraph1": "Enable CLI tutor mode to see shortcuts to the command-line version of common IPFS commands — helpful if you're learning to use IPFS from the terminal, or if you just need a refresher." + }, + "step7": { + "title": "IPFS Config", + "paragraph1": "You can change the config of your IPFS node right from Web UI!", + "paragraph2": "Don't forget to restart the daemon to apply the changes." + } + }, + "Experiments": { + "description": "Here you can get an early preview into new IPFS features by enabling options below. We are testing these ideas, and they may not be perfect yet so we'd love to hear your feedback.", + "issueUrl": "Open an issue", + "feedbackUrl": "💌Leave feedback", + "readMoreUrl": "Read More" + }, + "ipnsPublishingKeys": { + "title": "IPNS Publishing Keys", + "description": "The InterPlanetary Name System (IPNS) provides cryptographic addresses for publishing updates to content that is expected to change over time. This feature requires your node to be online at least once a day to ensure IPNS records are kept alive on the public DHT." + }, + "generateKeyModal": { + "title": "Generate New IPNS Key", + "description": "Enter a nickname for this key to generate:" + }, + "renameKeyModal": { + "title": "Rename Key", + "description": "Enter a new nickname for the IPNS key <1>{name}." + }, + "removeKeyModal": { + "title": "Confirm IPNS Key Removal", + "description": "Are you sure you want to delete the IPNS key <1>{name}? This operation can't be undone, and the IPNS address managed by this key will be lost FOREVER. Consider backing it up first: ipfs key export --help" + } +} diff --git a/public/locales/ko-KR/welcome.json b/public/locales/ko-KR/welcome.json new file mode 100644 index 000000000..297838ea6 --- /dev/null +++ b/public/locales/ko-KR/welcome.json @@ -0,0 +1,38 @@ +{ + "title": "Welcome | IPFS", + "description": "Welcome Page", + "connected": { + "paragraph1": "Welcome to the future of the internet! You are now a valuable part of the distributed web." + }, + "notConnected": { + "paragraph1": "<0>Check out the installation guide in the <1>IPFS Docs, or try these common fixes:", + "paragraph2": "<0>Is your IPFS daemon running? Try starting or restarting it from your terminal:", + "paragraph3": "<0>Is your IPFS API configured to allow <1>cross-origin (CORS) requests? If not, run these commands and then start your daemon from the terminal:", + "paragraph4": "<0>Is your IPFS API on a port other than 5001? If your node is configured with a <1>custom API address, enter it here." + }, + "aboutIpfs": { + "header": "What is IPFS?", + "paragraph1": "<0><0>A hypermedia distribution protocol that incorporates ideas from Kademlia, BitTorrent, Git, and more", + "paragraph2": "<0><0>A peer-to-peer file transfer network with a completely decentralized architecture and no central point of failure, censorship, or control", + "paragraph3": "<0><0>An on-ramp to tomorrow's web — traditional browsers can access IPFS files through gateways like <2>https://ipfs.io or directly using the <4>IPFS Companion extension", + "paragraph4": "<0><0>A next-gen CDN — just add a file to your node to make it available to the world with cache-friendly content-hash addressing and BitTorrent-style bandwidth distribution", + "paragraph5": "<0><0>A developer toolset for building <2>completely distributed apps and services, backed by a robust open-source community" + }, + "welcomeInfo": { + "header": "In this app, you can …", + "paragraph1": "<0><0>Check your node status, including how many peers you're connected to, your storage and bandwidth stats, and more", + "paragraph2": "<0><0>View and manage files in your IPFS repo, including drag-and-drop file import, easy pinning, and quick sharing and download options", + "paragraph3": "<0><0>Visit the \"Merkle Forest\" with some sample datasets and explore IPLD, the data model that underpins how IPFS works", + "paragraph4": "<0><0>See who's connected to your node, geolocated on a world map by their IP address", + "paragraph5": "<0><0>Review or edit your node settings — no command line required", + "paragraph6": "<0><0>Check this app's source code to <2>report a bug or make a contribution, and make IPFS better for everyone!" + }, + "tour": { + "step1": { + "title": "Welcome page", + "paragraph1": "This page lets you know if you're connected to IPFS, and offers ideas for things you can do in this app.", + "paragraph2": "If you aren't connected to the IPFS API, this page also appears in place of some other pages, with hints for how to get connected.", + "paragraph3": "You can visit this page from anywhere in the app by clicking the IPFS cube logo in the navigation bar." + } + } +} From db17cd828ac915f330dfe07fc88a6e8b76f3f21f Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:49:25 -0800 Subject: [PATCH 04/15] chore(i18n): ensure app:actions.add has ko translation --- public/locales/ko-KR/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/ko-KR/app.json b/public/locales/ko-KR/app.json index 142c477b2..5c3411096 100644 --- a/public/locales/ko-KR/app.json +++ b/public/locales/ko-KR/app.json @@ -1,6 +1,6 @@ { "actions": { - "add": "Add", + "add": "추가하다", "apply": "Apply", "browse": "Browse", "cancel": "Cancel", From fc24e9090938de6a9508f7399b34c427deb1261e Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:51:19 -0800 Subject: [PATCH 05/15] fix: only send i18n requests for current language Sends only a single request for lang via i18n-http-backend see https://github.com/i18next/i18next-http-backend/issues/61 --- src/i18n.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n.js b/src/i18n.js index eb245925b..d0ffca4b8 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -16,6 +16,7 @@ i18n .use(Backend) .use(LanguageDetector) .init({ + load: 'currentOnly', // see https://github.com/i18next/i18next-http-backend/issues/61 backend: { backends: [ LocalStorageBackend, From b3db8acc7b7f36c3f77f22ab408def8019cec4d6 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 11:53:14 -0800 Subject: [PATCH 06/15] fix: current language displays correctly for fallbacks --- src/lib/i18n.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib/i18n.js b/src/lib/i18n.js index d8336d6fa..321fd61ff 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -9,7 +9,23 @@ export const getCurrentLanguage = () => { export const getLanguage = (localeCode) => { if (!localeCode) return 'Unknown' - const info = languages[localeCode] + let info = languages[localeCode] + const fallbackLanguages = i18n.options.fallbackLng[localeCode] + + if (info == null && fallbackLanguages != null) { + /** + * check fallback languages before attempting to split a 'lang-COUNTRY' code + * fixed issue with displaying 'English' when i18nLng is set to 'ko' + * discovered when looking into https://github.com/ipfs/ipfs-webui/issues/2097 + */ + const fallback = fallbackLanguages + for (const locale of fallback) { + info = languages[locale] + if (info != null) { + return info.nativeName + } + } + } if (!info) { // if we haven't got the info in the `languages.json` we split it to get the language From cea0eee63925e6a394bf062065d9c10feb27dbd5 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 12:44:32 -0800 Subject: [PATCH 07/15] test(lib/i18n): test getLanguage function --- src/lib/i18n.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/lib/i18n.test.js diff --git a/src/lib/i18n.test.js b/src/lib/i18n.test.js new file mode 100644 index 000000000..0ef5d0a50 --- /dev/null +++ b/src/lib/i18n.test.js @@ -0,0 +1,27 @@ +/* global describe, it, expect, beforeAll, afterAll */ +// @ts-check + +import { getLanguage } from './i18n.js' +import languages from './languages.json' + +describe('i18n', function () { + describe('getLanguage', function () { + it('returns unknown when given non-truthy input', () => { + expect(getLanguage()).toBe('Unknown') + expect(getLanguage('')).toBe('Unknown') + expect(getLanguage(null)).toBe('Unknown') + expect(getLanguage(undefined)).toBe('Unknown') + }) + + describe('returns the correct nativeName for each language', () => { + Object.keys(languages).forEach((lang) => { + it(`returns ${languages[lang].nativeName} for ${lang}`, () => { + expect(getLanguage(lang)).toBe(languages[lang].nativeName) + }) + }) + }) + }) + describe('getCurrentLanguage', function () { + + }) +}) From ad2cb3e66e9331890a02339d6b297c5aeb32bb29 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 12:54:06 -0800 Subject: [PATCH 08/15] test(lib/i18n): add getCurrentLanguage test --- src/lib/i18n.test.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/lib/i18n.test.js b/src/lib/i18n.test.js index 0ef5d0a50..81f3eefd1 100644 --- a/src/lib/i18n.test.js +++ b/src/lib/i18n.test.js @@ -1,9 +1,13 @@ /* global describe, it, expect, beforeAll, afterAll */ // @ts-check - -import { getLanguage } from './i18n.js' +import i18n from '../i18n.js' +import { getLanguage, getCurrentLanguage } from './i18n.js' import languages from './languages.json' +const testEachLanguage = (fn) => { + Object.keys(languages).forEach((lang) => fn(lang)) +} + describe('i18n', function () { describe('getLanguage', function () { it('returns unknown when given non-truthy input', () => { @@ -14,14 +18,29 @@ describe('i18n', function () { }) describe('returns the correct nativeName for each language', () => { - Object.keys(languages).forEach((lang) => { + testEachLanguage((lang) => { it(`returns ${languages[lang].nativeName} for ${lang}`, () => { expect(getLanguage(lang)).toBe(languages[lang].nativeName) }) }) }) }) + describe('getCurrentLanguage', function () { + it('returns unknown when i18n isn\'t initialized', () => { + expect(getCurrentLanguage()).toBe('Unknown') + }) + describe('returns the correct nativeName for each language', () => { + beforeAll(async function () { + await i18n.init() + }) + testEachLanguage((lang) => { + it(`returns ${languages[lang].nativeName} for ${lang}`, async () => { + await i18n.changeLanguage(lang) + expect(getCurrentLanguage()).toBe(languages[lang].nativeName) + }) + }) + }) }) }) From cc3c9e96b8d7d3642a9ee675ffb4a43b703a38d8 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:00:08 -0800 Subject: [PATCH 09/15] test(i18n): add test for naming languages in languages.json --- src/i18n.test.js | 7 ++----- src/lib/i18n.test.js | 16 +++++++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/i18n.test.js b/src/i18n.test.js index 833ae2fb2..455034f3d 100644 --- a/src/i18n.test.js +++ b/src/i18n.test.js @@ -1,15 +1,12 @@ /* global describe, it, expect, beforeAll, afterAll */ // @ts-check import { createServer } from 'http-server' -import i18n from './i18n.js' -import { readdir } from 'node:fs/promises' +import i18n, { localesList } from './i18n.js' import getPort from 'get-port' const backendListenerPort = await getPort({ port: getPort.makeRange(3000, 4000) }) -const allLanguages = (await readdir('./public/locales', { withFileTypes: true })) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name) +const allLanguages = localesList.map(({ locale }) => locale) /** * @type {import('http-server').HTTPServer} diff --git a/src/lib/i18n.test.js b/src/lib/i18n.test.js index 81f3eefd1..5a45bee5e 100644 --- a/src/lib/i18n.test.js +++ b/src/lib/i18n.test.js @@ -1,14 +1,24 @@ /* global describe, it, expect, beforeAll, afterAll */ // @ts-check -import i18n from '../i18n.js' +import i18n, { localesList } from '../i18n.js' import { getLanguage, getCurrentLanguage } from './i18n.js' import languages from './languages.json' +import { readdir } from 'node:fs/promises' const testEachLanguage = (fn) => { Object.keys(languages).forEach((lang) => fn(lang)) } +const allLanguages = (await readdir('./public/locales', { withFileTypes: true })) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name) + describe('i18n', function () { + it('should have a languages.json entry for each folder', function () { + const namedLocales = localesList.map(({ locale }) => locale) + expect(namedLocales).toEqual(allLanguages) + }) + describe('getLanguage', function () { it('returns unknown when given non-truthy input', () => { expect(getLanguage()).toBe('Unknown') @@ -27,10 +37,6 @@ describe('i18n', function () { }) describe('getCurrentLanguage', function () { - it('returns unknown when i18n isn\'t initialized', () => { - expect(getCurrentLanguage()).toBe('Unknown') - }) - describe('returns the correct nativeName for each language', () => { beforeAll(async function () { await i18n.init() From b7657da144be8e99b75dde7b60ee557bfb9480ed Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 14:20:17 -0800 Subject: [PATCH 10/15] fix(i18n): add parser for getting valid locale codes --- src/lib/i18n-localeParser.js | 46 ++++++++++++++++ src/lib/i18n-localeParser.test.js | 90 +++++++++++++++++++++++++++++++ src/lib/i18n.test.js | 2 +- 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/lib/i18n-localeParser.js create mode 100644 src/lib/i18n-localeParser.test.js diff --git a/src/lib/i18n-localeParser.js b/src/lib/i18n-localeParser.js new file mode 100644 index 000000000..4ff8fce82 --- /dev/null +++ b/src/lib/i18n-localeParser.js @@ -0,0 +1,46 @@ +/** + * + * @param {object} options + * @param {import('i18next').i18n} options.i18n + * @param {string} options.localeCode + * @param {Record} options.languages + * + * @returns {string} + */ +export default function getValidLocaleCode ({ i18n, localeCode, languages }) { + const info = languages[localeCode] + + if (info != null) { + return localeCode + } + + const fallbackLanguages = i18n.options.fallbackLng[localeCode] + if (info == null && fallbackLanguages != null) { + /** + * check fallback languages before attempting to split a 'lang-COUNTRY' code + * fixed issue with displaying 'English' when i18nLng is set to 'ko' + * discovered when looking into https://github.com/ipfs/ipfs-webui/issues/2097 + */ + const fallback = fallbackLanguages + for (const locale of fallback) { + const fallbackInfo = languages[locale] + + if (fallbackInfo != null) { + return fallbackInfo.locale + } + } + } + + // if we haven't got the info in the `languages.json` we split it to get the language + const langOnly = localeCode.split('-')[0] + if (languages[langOnly]) { + return langOnly + } + // if the provided localeCode doesn't have country, but we have a supported language for a specific country, we return that + const langWithCountry = Object.keys(languages).find((key) => key.startsWith(localeCode)) + if (langWithCountry) { + return langWithCountry + } + + return 'en' +} diff --git a/src/lib/i18n-localeParser.test.js b/src/lib/i18n-localeParser.test.js new file mode 100644 index 000000000..92aed20d7 --- /dev/null +++ b/src/lib/i18n-localeParser.test.js @@ -0,0 +1,90 @@ +/* global describe, it, expect */ +// @ts-check +import i18n from '../i18n.js' +import getValidLocaleCode from './i18n-localeParser.js' +import languages from './languages.json' + +const fallbackLanguages = /** @type {import('i18next').FallbackLngObjList} */(i18n.options.fallbackLng) + +const testEachLanguage = (fn) => { + Object.keys(languages) + .concat(Object.keys(fallbackLanguages).filter((lang) => lang !== 'default')) + .concat([ + // any extra language codes you want to test go here. It will need a corresponding entry in the expectedMap below + ]) + .forEach((lang) => fn(lang)) +} + +const expectedMap = new Proxy({ + /** + * This should ideally be handled by the fallbackLng option in i18n.js, but if it needs overriding, it can be done here. + * @example + * 'en-GB': 'en' + */ +}, { + /** + * + * @param {Record} target + * @param {string} prop + * @returns {string} + */ + get: (target, prop) => { + const result = target[prop] + if (result != null) { + return result + } + const fallbackResult = fallbackLanguages[prop] + if (fallbackResult != null) { + return fallbackResult[0] + } + + return prop + } +}) + +describe('i18n-localeParser', function () { + it('returns en for unsupported locale', () => { + expect(getValidLocaleCode({ i18n, localeCode: 'xx', languages })).toBe('en') // invalid locale code + expect(getValidLocaleCode({ i18n, localeCode: 'el-GR', languages })).toBe('en') // Greek is not supported + }) + + // graceful degradation: Use the most specific locale that is supported + it('returns `lang` only if `lang-COUNTRY` is unsupported', () => { + // arabic is supported, but arabic-saudi arabia is not + expect(getValidLocaleCode({ i18n, localeCode: 'ar-SA', languages })).toBe('ar') + // Catalan is supported, but Catalan-France is not + expect(getValidLocaleCode({ i18n, localeCode: 'ca-FR', languages })).toBe('ca') + // Czech is supported, but Czech-Czech is not + expect(getValidLocaleCode({ i18n, localeCode: 'cs-CZ', languages })).toBe('cs') + // Danish is supported, but Danish-Denmark is not + expect(getValidLocaleCode({ i18n, localeCode: 'da-DK', languages })).toBe('da') + // German is supported, but German-Germany is not + expect(getValidLocaleCode({ i18n, localeCode: 'de-DE', languages })).toBe('de') + // Spanish is supported, but Spanish-Spain is not + expect(getValidLocaleCode({ i18n, localeCode: 'es-ES', languages })).toBe('es') + // Finnish is supported, but Finnish-Finland is not + expect(getValidLocaleCode({ i18n, localeCode: 'fi-FI', languages })).toBe('fi') + // French is supported, but French-France is not + expect(getValidLocaleCode({ i18n, localeCode: 'fr-FR', languages })).toBe('fr') + }) + + // graceful degradation 2: Use a more specific locale if general locale is not supported + it('returns `lang-COUNTRY` if `lang` is unsupported', () => { + // Hindi is not supported, but Hindi-India is + expect(getValidLocaleCode({ i18n, localeCode: 'hi', languages })).toBe('hi-IN') + // Japanese is not supported, but Japanese-Japan is + expect(getValidLocaleCode({ i18n, localeCode: 'ja', languages })).toBe('ja-JP') + // Korean is not supported, but Korean-Korea is + expect(getValidLocaleCode({ i18n, localeCode: 'ko', languages })).toBe('ko-KR') + // Chinese is not supported without country code, but Chinese-China, Chinese-Hong Kong, and Chinese-Taiwan are. We default to Chinese-China + expect(getValidLocaleCode({ i18n, localeCode: 'zh', languages })).toBe('zh-CN') + }) + + testEachLanguage((lang) => { + const expectedValue = expectedMap[lang] + it(`returns ${expectedValue} for ${lang}`, () => { + const actualValue = getValidLocaleCode({ i18n, localeCode: lang, languages }) + expect(actualValue).toBe(expectedValue) + }) + }) +}) diff --git a/src/lib/i18n.test.js b/src/lib/i18n.test.js index 5a45bee5e..3009fafe4 100644 --- a/src/lib/i18n.test.js +++ b/src/lib/i18n.test.js @@ -1,4 +1,4 @@ -/* global describe, it, expect, beforeAll, afterAll */ +/* global describe, it, expect, beforeAll */ // @ts-check import i18n, { localesList } from '../i18n.js' import { getLanguage, getCurrentLanguage } from './i18n.js' From 54749f4eaa75e7547114b096bfe86bf4816f9d6a Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 14:21:23 -0800 Subject: [PATCH 11/15] fix(lib/i18n): use i18n-localeParser --- src/lib/i18n.js | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/src/lib/i18n.js b/src/lib/i18n.js index 321fd61ff..f21bd1522 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -1,6 +1,7 @@ import i18n from '../i18n.js' // languages.json generated from our locale dir via https://github.com/olizilla/lol import languages from './languages.json' +import getValidLocaleCode from './i18n-localeParser.js' export const getCurrentLanguage = () => { return getLanguage(i18n.language) @@ -9,30 +10,7 @@ export const getCurrentLanguage = () => { export const getLanguage = (localeCode) => { if (!localeCode) return 'Unknown' - let info = languages[localeCode] - const fallbackLanguages = i18n.options.fallbackLng[localeCode] + const correctLocaleCode = getValidLocaleCode({ i18n, localeCode, languages }) - if (info == null && fallbackLanguages != null) { - /** - * check fallback languages before attempting to split a 'lang-COUNTRY' code - * fixed issue with displaying 'English' when i18nLng is set to 'ko' - * discovered when looking into https://github.com/ipfs/ipfs-webui/issues/2097 - */ - const fallback = fallbackLanguages - for (const locale of fallback) { - info = languages[locale] - if (info != null) { - return info.nativeName - } - } - } - - if (!info) { - // if we haven't got the info in the `languages.json` we split it to get the language - const lang = languages[localeCode.split('-')[0]] - // if we have the language we add it, else we fallback to english (Web UI default lang) - return (lang && lang.nativeName) || languages.en.englishName - } - - return info.nativeName + return languages[correctLocaleCode].nativeName } From 7eb9327ab1c8ab04b23cc6be50c299682589fc35 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 14:22:33 -0800 Subject: [PATCH 12/15] fix(i18n): prevent the lookup of invalid locales fixes https://github.com/ipfs/ipfs-webui/issues/2097 --- src/i18n.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index d0ffca4b8..e405600ec 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -7,6 +7,7 @@ import LanguageDetector from 'i18next-browser-languagedetector' import pkgJson from '../package.json' import locales from './lib/languages.json' +import getValidLocaleCode from './lib/i18n-localeParser.js' const { version } = pkgJson export const localesList = Object.values(locales) @@ -28,8 +29,11 @@ i18n expirationTime: (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') ? 1 : 7 * 24 * 60 * 60 * 1000 }, { // HttpBackend - // ensure a relative path is used to look up the locales, so it works when loaded from /ipfs/ - loadPath: 'locales/{{lng}}/{{ns}}.json' + loadPath: (lngs, namespaces) => { + const locale = getValidLocaleCode({ i18n, localeCode: lngs[0], languages: locales }) + // ensure a relative path is used to look up the locales, so it works when loaded from /ipfs/ + return `locales/${locale}/${namespaces}.json` + } } ] }, From fb5995328dd599b6ae1e4f6d33043589ff51f913 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 15:13:04 -0800 Subject: [PATCH 13/15] test(e2e:settings): test language selector --- .../language-selector/LanguageSelector.js | 6 +-- .../language-modal/LanguageModal.js | 2 +- test/e2e/settings.test.js | 38 ++++++++++++++++++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/components/language-selector/LanguageSelector.js b/src/components/language-selector/LanguageSelector.js index b04bf3ffa..3518cffad 100644 --- a/src/components/language-selector/LanguageSelector.js +++ b/src/components/language-selector/LanguageSelector.js @@ -19,16 +19,16 @@ class LanguageSelector extends Component { return (
-
+
{getCurrentLanguage()}
-
- + ) diff --git a/src/components/language-selector/language-modal/LanguageModal.js b/src/components/language-selector/language-modal/LanguageModal.js index ec1061d9e..6a1e63f68 100644 --- a/src/components/language-selector/language-modal/LanguageModal.js +++ b/src/components/language-selector/language-modal/LanguageModal.js @@ -25,7 +25,7 @@ const LanguageModal = ({ t, tReady, onLeave, link, className, isIpfsDesktop, doD { localesList.map((lang) => diff --git a/test/e2e/settings.test.js b/test/e2e/settings.test.js index a0a89a8ef..858ad9e81 100644 --- a/test/e2e/settings.test.js +++ b/test/e2e/settings.test.js @@ -1,4 +1,15 @@ -import { test } from './setup/coverage.js' +import { readFile } from 'node:fs/promises' + +import { test, expect } from './setup/coverage.js' + +const languageFilePromise = readFile('./src/lib/languages.json', 'utf8') + +let languages +const getLanguages = async () => { + if (languages != null) return languages + languages = JSON.parse(await languageFilePromise) + return languages +} test.describe('Settings screen', () => { test.beforeEach(async ({ page }) => { @@ -13,4 +24,29 @@ test.describe('Settings screen', () => { const id = process.env.IPFS_RPC_ID await page.waitForSelector(`text=${id}`) }) + + test('Language selector', async ({ page }) => { + const languages = await getLanguages() + for (const lang of Object.values(languages).map((lang) => lang.locale)) { + const changeLanguageBtn = await page.waitForSelector('.e2e-languageSelector-changeBtn') + await changeLanguageBtn.click() + + // wait for the language modal to appear + await page.waitForSelector('.e2e-languageModal') + + // select the language + const languageModalButton = await page.waitForSelector(`.e2e-languageModal-lang_${lang}`) + await languageModalButton.click() + + // wait for the language modal to disappear + await page.waitForSelector('.e2e-languageModal', { state: 'hidden' }) + + // check that the language has changed + await page.waitForSelector('.e2e-languageSelector-current', { text: languages[lang].nativeName }) + + // confirm the localStorage setting was applied + const i18nLang = await page.evaluate('localStorage.getItem(\'i18nextLng\')') + expect(i18nLang).toBe(lang) + } + }) }) From c81c2a22ab55ade4505e371598f86514508394b6 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 15:26:15 -0800 Subject: [PATCH 14/15] test(e2e/settings): validate that language files are requested --- test/e2e/settings.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/e2e/settings.test.js b/test/e2e/settings.test.js index 858ad9e81..59dc2b123 100644 --- a/test/e2e/settings.test.js +++ b/test/e2e/settings.test.js @@ -28,18 +28,32 @@ test.describe('Settings screen', () => { test('Language selector', async ({ page }) => { const languages = await getLanguages() for (const lang of Object.values(languages).map((lang) => lang.locale)) { + // click the 'change language' button const changeLanguageBtn = await page.waitForSelector('.e2e-languageSelector-changeBtn') await changeLanguageBtn.click() // wait for the language modal to appear await page.waitForSelector('.e2e-languageModal') + // create a promise that resolves when the request for the new translation file is made + const requestForNewTranslationFiles = page.waitForRequest((request) => { + if (lang === 'en') { + // english is the fallback language and we can't guarantee the request wasn't already made, so we resolve for 'en' on any request + + return true + } + const url = request.url() + + return url.includes(`locales/${lang}`) && url.includes('.json') + }) + // select the language const languageModalButton = await page.waitForSelector(`.e2e-languageModal-lang_${lang}`) await languageModalButton.click() // wait for the language modal to disappear await page.waitForSelector('.e2e-languageModal', { state: 'hidden' }) + await requestForNewTranslationFiles // check that the language has changed await page.waitForSelector('.e2e-languageSelector-current', { text: languages[lang].nativeName }) From 594e0ba845a44accf26de1a2b073e8947671ecd0 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 23 Feb 2023 15:59:37 -0800 Subject: [PATCH 15/15] chore: remove untranslated ko-KR files --- public/locales/ko-KR/notify.json | 30 ----- public/locales/ko-KR/settings.json | 184 ----------------------------- public/locales/ko-KR/welcome.json | 38 ------ 3 files changed, 252 deletions(-) delete mode 100644 public/locales/ko-KR/notify.json delete mode 100644 public/locales/ko-KR/settings.json delete mode 100644 public/locales/ko-KR/welcome.json diff --git a/public/locales/ko-KR/notify.json b/public/locales/ko-KR/notify.json deleted file mode 100644 index 774ede1fd..000000000 --- a/public/locales/ko-KR/notify.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "ipfsApiRequestFailed": "Could not connect. Please check if your daemon is running.", - "windowIpfsRequestFailed": "IPFS request failed. Please check your IPFS Companion settings.", - "ipfsInvalidApiAddress": "The provided IPFS API address is invalid.", - "ipfsConnectSuccess": "Successfully connected to the IPFS API address", - "ipfsConnectFail": "Unable to connect to the provided IPFS API address", - "ipfsPinFailReason": "Unable to set pinning at {serviceName}: {errorMsg}", - "ipfsPinSucceedReason": "Successfully pinned at {serviceName}", - "ipfsUnpinSucceedReason": "Successfully unpinned from {serviceName}", - "ipfsIsBack": "Normal IPFS service has resumed. Enjoy!", - "folderExists": "An item with that name already exists. Please choose another.", - "filesFetchFailed": "Failed to get those files. Please check the path and try again.", - "filesRenameFailed": "Failed to rename the files. Please try again.", - "filesMakeDirFailed": "Failed to create directory. Please try again.", - "filesCopyFailed": "Failed to copy the files. Please try again.", - "filesRemoveFailed": "Failed to remove the files. Please try again.", - "filesAddFailed": "Failed to add files to IPFS. Please try again.", - "filesEventFailed": "Failed to process your request. Please try again.", - "couldntConnectToPeer": "Could not connect to the provided peer.", - "connectedToPeer": "Successfully connected to the provided peer.", - "experimentsErrors": { - "npmOnIpfs": "We couldn't make changes to your system. Please install or uninstall 'ipfs-npm' package manually." - }, - "desktopToggleErrors": { - "autoLaunch": "Could not toggle launch at startup option.", - "ipfsOnPath": "Could not toggle IPFS command line tools.", - "downloadHashShortcut": "Could not toggle download hash shortcut.", - "screenshotShortcut": "Could not toggle screenshot shortcut." - } -} diff --git a/public/locales/ko-KR/settings.json b/public/locales/ko-KR/settings.json deleted file mode 100644 index 41f1245e9..000000000 --- a/public/locales/ko-KR/settings.json +++ /dev/null @@ -1,184 +0,0 @@ -{ - "title": "Settings", - "save": "Save", - "saving": "Saving…", - "reset": "Reset", - "learnMoreLink": "Learn more.", - "showOptions": "Show options", - "pinningServices": { - "title": "Pinning Services", - "description": "Use local pinning when you want to ensure an item on your node is never garbage-collected, even if you remove it from Files. You can also link your accounts with other remote pinning services to automatically or selectively persist files with those providers, enabling you to keep backup copies of your files and/or make them available to others when your local node is offline.", - "noPinRemoteDescription": "Support for third-party remote pinning services is missing. Update your IPFS node to Kubo 0.8.0 or later, and you'll be able to add and configure them here.", - "addAutoUpload": "Enable auto upload", - "removeAutoUpload": "Disable Auto Upload" - }, - "language": "Language", - "analytics": "Analytics", - "cliTutorMode": "CLI Tutor Mode", - "config": "IPFS Config", - "languageModal": { - "title": "Change language", - "description": "Pick your preferred language.", - "translationProjectIntro": "Add or improve translations and make IPFS better for everyone!", - "translationProjectLink": "Join the IPFS Translation Project" - }, - "apiDescription": "<0>If your node is configured with a <1>custom API address, including a port other than the default 5001, enter it here.", - "publicGatewayDescription": "<0>Choose which <1>public gateway you want to use when generating shareable links.", - "cliDescription": "<0>Enable this option to display a \"view code\" <1> icon next to common IPFS commands. Clicking it opens a modal with that command's CLI code, so you can paste it into the IPFS command-line interface in your terminal.", - "cliModal": { - "extraNotesJsonConfig": "If you've made changes to the config in this page's code editor that you'd like to save, click the download icon next to the copy button to download it as a JSON file." - }, - "pinningModal": { - "title": "Select a pinning service provider.", - "description": "Don’t see your pinning service provider? <1>Add a custom one.<1>" - }, - "pinningServiceModal": { - "title": "Configure a custom pinning service.", - "description": "Want to make your custom pinning service available to others? <1>Learn how.<1>", - "service": "Service", - "nickname": "Nickname", - "nicknamePlaceholder": "Name for your service", - "apiEndpoint": "API endpoint", - "apiEndpointPlaceholder": "URL for its API endpoint", - "secretApiKey": "Secret access token", - "secretApiKeyHowToLink": "How to generate a new token", - "autoUpload": "Auto upload", - "complianceLabel": "🔍 {nickname} pinning compliance report" - }, - "autoUploadModal": { - "title": "Change upload policy for {name}", - "description": "When enabled, auto upload will periodically pin the root of your Files directory, ensuring everything is pinned remotely and available to other peers even when your local IPFS node goes offline. If you disable it, you can still pin each file manually using the \"Set pinning\" option on the Files screen." - }, - "errors": { - "nickname": "Nickname is required", - "apiError": "API error", - "apiEndpoint": "Must be a valid URL", - "secretApiKey": "Secret access token is required", - "failedToFetch": "Failed to fetch", - "failedToFetchTitle": "Unable to fetch pin count from this remote service. Make sure it is online and that you entered correct credentials. If this is a newly added service, try removing and adding it again." - }, - "actions": { - "addService": "Add Service", - "generateKey": "Generate Key", - "edit": "Change", - "close": "Close", - "save": "Save", - "cancel": "Cancel", - "enable": "Enable", - "disable": "Disable" - }, - "edit": "Edit", - "visitService": "Visit service", - "remove": "Remove", - "localPinning": "Local Pinning", - "service": "Service", - "size": "Size", - "pins": "Pins", - "autoUpload": "Auto Upload", - "autoUploadPolicy": { - "true": "All files", - "false": "Off" - }, - "fetchingSettings": "Fetching settings...", - "configApiNotAvailable": "The IPFS config API is not available. Please disable the \"IPFS Companion\" Web Extension and try again.", - "ipfsDaemonOffline": "The IPFS daemon is offline. Please turn it on and try again.", - "settingsUnavailable": "Settings not available. Please check your IPFS daemon is running.", - "settingsHaveChanged": "The settings have changed; please click <1>Reset to update the editor contents.", - "errorOccured": "An error occured while saving your changes", - "checkConsole": "Check the browser console for more info.", - "changesSaved": "Your changes have been saved.", - "settingsWillBeUsedNextTime": "The new settings will be used next time you restart the IPFS daemon.", - "ipfsConfigDescription": "The IPFS config file is a JSON document. It is read once when the IPFS daemon is started. Save your changes, then restart the IPFS daemon to apply them.", - "ipfsConfigHelp": "Check the documentation for further information.", - "AnalyticsToggle": { - "label": "Help improve this app by sending anonymous usage data", - "summary": "Configure what is collected", - "paragraph1": "No CIDs, filenames, or other personal information are collected. We want metrics to show us which features are useful to help us prioritise what to work on next, and system configuration information to guide our testing.", - "paragraph2": "Protocol Labs hosts a <1>Countly instance to record anonymous usage data for this app.", - "basicInfo": "The following additional information is sent to countly.ipfs.io", - "optionalInfo": "You can opt-in to send the following information:", - "sessions": { - "label": "Sessions", - "summary": "When and how long you use the app, and browser metrics", - "details": "<0>The following browser metrics are sent:<1><0>A random, generated device ID<1>Timestamp when the session starts<2>Periodic timestamps to track duration<3>App version; e.g. 2.4.4<4>Locale; e.g. en-GB<5>User agent; e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) …<6>Screen resolution; e.g. 800×600<7>Screen pixel density; e.g. 1" - }, - "events": { - "label": "Events", - "summary": "actions like adding or deleting files, and how long it took", - "details": "App-specific actions. We record only that the action happened, how long it took from start to finish, and a count if the event involved multiple items." - }, - "views": { - "label": "Page views", - "summary": "Which sections of the app you visit", - "details": "Records which sections of the apps are visited. The paths recorded only include the pattern used to match the route rather than the full url." - }, - "location": { - "label": "Location", - "summary": "Country code from IP address (IP address is discarded)", - "details": "Your IP address is used to calculate a country code for your location, like \"FR\" for France. Where consent is not given to track location, an empty location property is sent with all requests to signal to the server to disable the location look up. This information helps guide our translation effort and figure out where we should put on events." - }, - "crashes": { - "label": "App errors", - "summary": "JavaScript error messages and stack traces", - "details": "Records JavaScript error messages and stack traces that occur while using the app, where possible. It is very helpful to know when the app is not working for you, but <1>error messages may include identifiable information like CIDs or file paths, so only enable this if you are comfortable sharing that information with us." - } - }, - "cliToggle": { - "label": "Enable command-line interface (CLI) tutor mode" - }, - "tour": { - "step1": { - "title": "Settings page", - "paragraph1": "Here you can change the settings of your Web UI and IPFS node.", - "paragraph2": "If you're running IPFS Desktop you'll have some specific settings for it too." - }, - "step2": { - "title": "Custom API address", - "paragraph1": "If you've configured your IPFS node with a custom API address, you can update your config file right here instead of editing the JSON by hand." - }, - "step3": { - "title": "Pinning services", - "paragraph1": "<0>If you have accounts with third-party remote pinning services, add them here so you can pin/unpin items to those services directly from the Files screen. You can learn more about third-party pinning services in the <1>IPFS Docs." - }, - "step4": { - "title": "Language selector", - "paragraph1": "<0>You can change the language of the Web UI. If your preferred language isn't available, head over our project page in <1>Transifex to help us translate!" - }, - "step5": { - "title": "Anonymous usage analytics", - "paragraph1": "If you opt-in, you can help us make the Web UI better by sending anonymous usage analytics.", - "paragraph2": "You're able to choose what data you send us and we won't be able to identify you, we value privacy above all else." - }, - "step6": { - "title": "CLI tutor mode", - "paragraph1": "Enable CLI tutor mode to see shortcuts to the command-line version of common IPFS commands — helpful if you're learning to use IPFS from the terminal, or if you just need a refresher." - }, - "step7": { - "title": "IPFS Config", - "paragraph1": "You can change the config of your IPFS node right from Web UI!", - "paragraph2": "Don't forget to restart the daemon to apply the changes." - } - }, - "Experiments": { - "description": "Here you can get an early preview into new IPFS features by enabling options below. We are testing these ideas, and they may not be perfect yet so we'd love to hear your feedback.", - "issueUrl": "Open an issue", - "feedbackUrl": "💌Leave feedback", - "readMoreUrl": "Read More" - }, - "ipnsPublishingKeys": { - "title": "IPNS Publishing Keys", - "description": "The InterPlanetary Name System (IPNS) provides cryptographic addresses for publishing updates to content that is expected to change over time. This feature requires your node to be online at least once a day to ensure IPNS records are kept alive on the public DHT." - }, - "generateKeyModal": { - "title": "Generate New IPNS Key", - "description": "Enter a nickname for this key to generate:" - }, - "renameKeyModal": { - "title": "Rename Key", - "description": "Enter a new nickname for the IPNS key <1>{name}." - }, - "removeKeyModal": { - "title": "Confirm IPNS Key Removal", - "description": "Are you sure you want to delete the IPNS key <1>{name}? This operation can't be undone, and the IPNS address managed by this key will be lost FOREVER. Consider backing it up first: ipfs key export --help" - } -} diff --git a/public/locales/ko-KR/welcome.json b/public/locales/ko-KR/welcome.json deleted file mode 100644 index 297838ea6..000000000 --- a/public/locales/ko-KR/welcome.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "title": "Welcome | IPFS", - "description": "Welcome Page", - "connected": { - "paragraph1": "Welcome to the future of the internet! You are now a valuable part of the distributed web." - }, - "notConnected": { - "paragraph1": "<0>Check out the installation guide in the <1>IPFS Docs, or try these common fixes:", - "paragraph2": "<0>Is your IPFS daemon running? Try starting or restarting it from your terminal:", - "paragraph3": "<0>Is your IPFS API configured to allow <1>cross-origin (CORS) requests? If not, run these commands and then start your daemon from the terminal:", - "paragraph4": "<0>Is your IPFS API on a port other than 5001? If your node is configured with a <1>custom API address, enter it here." - }, - "aboutIpfs": { - "header": "What is IPFS?", - "paragraph1": "<0><0>A hypermedia distribution protocol that incorporates ideas from Kademlia, BitTorrent, Git, and more", - "paragraph2": "<0><0>A peer-to-peer file transfer network with a completely decentralized architecture and no central point of failure, censorship, or control", - "paragraph3": "<0><0>An on-ramp to tomorrow's web — traditional browsers can access IPFS files through gateways like <2>https://ipfs.io or directly using the <4>IPFS Companion extension", - "paragraph4": "<0><0>A next-gen CDN — just add a file to your node to make it available to the world with cache-friendly content-hash addressing and BitTorrent-style bandwidth distribution", - "paragraph5": "<0><0>A developer toolset for building <2>completely distributed apps and services, backed by a robust open-source community" - }, - "welcomeInfo": { - "header": "In this app, you can …", - "paragraph1": "<0><0>Check your node status, including how many peers you're connected to, your storage and bandwidth stats, and more", - "paragraph2": "<0><0>View and manage files in your IPFS repo, including drag-and-drop file import, easy pinning, and quick sharing and download options", - "paragraph3": "<0><0>Visit the \"Merkle Forest\" with some sample datasets and explore IPLD, the data model that underpins how IPFS works", - "paragraph4": "<0><0>See who's connected to your node, geolocated on a world map by their IP address", - "paragraph5": "<0><0>Review or edit your node settings — no command line required", - "paragraph6": "<0><0>Check this app's source code to <2>report a bug or make a contribution, and make IPFS better for everyone!" - }, - "tour": { - "step1": { - "title": "Welcome page", - "paragraph1": "This page lets you know if you're connected to IPFS, and offers ideas for things you can do in this app.", - "paragraph2": "If you aren't connected to the IPFS API, this page also appears in place of some other pages, with hints for how to get connected.", - "paragraph3": "You can visit this page from anywhere in the app by clicking the IPFS cube logo in the navigation bar." - } - } -}