diff --git a/app/folder/[id].tsx b/app/folder/[id].tsx index 4261fbe..dbefc57 100644 --- a/app/folder/[id].tsx +++ b/app/folder/[id].tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, Alert, ScrollView, StyleSheet, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { t } from '../../src/i18n'; import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'; import * as Haptics from 'expo-haptics'; import { LibraryHeader } from '../../src/components/library/LibraryHeader'; @@ -91,7 +92,7 @@ export default function FolderScreen() { } } catch (error) { if (__DEV__) console.warn('[FolderScreen] create note failed', error); - Alert.alert('Could not create note', 'Please try again.'); + Alert.alert(t.library.couldNotCreateNote, t.common.pleaseTryAgain); } finally { creatingNoteRef.current = false; } @@ -99,12 +100,12 @@ export default function FolderScreen() { const confirmDeleteNote = useCallback((note: NoteMetadata) => { Alert.alert( - 'Delete note?', - `"${note.title}" will be permanently deleted.`, + t.library.deleteNoteTitle, + t.library.deleteNoteMessage(note.title), [ - { text: 'Cancel', style: 'cancel' }, + { text: t.common.cancel, style: 'cancel' }, { - text: 'Delete', + text: t.common.delete, style: 'destructive', onPress: async () => { try { @@ -112,7 +113,7 @@ export default function FolderScreen() { await refresh(); } catch (error) { if (__DEV__) console.warn('[FolderScreen] delete note failed', error); - Alert.alert('Could not delete note', 'Please try again.'); + Alert.alert(t.library.couldNotDeleteNote, t.common.pleaseTryAgain); } }, }, @@ -127,7 +128,7 @@ export default function FolderScreen() { style={[styles.flex, { backgroundColor: theme.colors.background }]} > router.replace('/')} /> @@ -141,7 +142,7 @@ export default function FolderScreen() { return ( router.replace('/')} rightIcon="create-outline" @@ -150,8 +151,8 @@ export default function FolderScreen() { {notes.length === 0 ? ( ) : ( @@ -194,12 +195,12 @@ export default function FolderScreen() { ? [ { key: 'rename', - label: 'Rename', + label: t.common.rename, onPress: () => setAction({ kind: 'renameNote', note: action.note }), }, { key: 'moveRoot', - label: 'Move to Root', + label: t.library.moveToRoot, onPress: async () => { await moveNote(action.note.id, null); await refresh(); @@ -207,12 +208,12 @@ export default function FolderScreen() { }, { key: 'move', - label: 'Move to Folder', + label: t.library.moveToFolder, onPress: () => setAction({ kind: 'moveNote', note: action.note }), }, { key: 'delete', - label: 'Delete', + label: t.common.delete, destructive: true, onPress: () => confirmDeleteNote(action.note), }, @@ -224,9 +225,9 @@ export default function FolderScreen() { setAction(null)} onConfirm={async (value) => { if (action?.kind === 'renameNote') { @@ -239,9 +240,9 @@ export default function FolderScreen() { setAction(null)} onConfirm={async (value) => { if (folder) { diff --git a/app/index.tsx b/app/index.tsx index cde2f65..c4e8ea7 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -42,6 +42,7 @@ import { renameFolder, } from '../src/services/foldersRepo'; import type { BackgroundType, FolderMetadata, NoteMetadata } from '../src/types/note'; +import { t } from '../src/i18n'; type Action = | { kind: 'newItem' } @@ -160,7 +161,7 @@ export default function LibraryScreen() { } } catch (error) { if (__DEV__) console.warn('[LibraryScreen] create note failed', error); - Alert.alert('Could not create note', 'Please try again.'); + Alert.alert(t.library.couldNotCreateNote, t.common.pleaseTryAgain); } finally { creatingNoteRef.current = false; } @@ -172,12 +173,12 @@ export default function LibraryScreen() { const confirmDeleteNote = useCallback((note: NoteMetadata) => { Alert.alert( - 'Delete note?', - `"${note.title}" will be permanently deleted.`, + t.library.deleteNoteTitle, + t.library.deleteNoteMessage(note.title), [ - { text: 'Cancel', style: 'cancel' }, + { text: t.common.cancel, style: 'cancel' }, { - text: 'Delete', + text: t.common.delete, style: 'destructive', onPress: async () => { try { @@ -185,7 +186,7 @@ export default function LibraryScreen() { await refresh(); } catch (error) { if (__DEV__) console.warn('[LibraryScreen] delete note failed', error); - Alert.alert('Could not delete note', 'Please try again.'); + Alert.alert(t.library.couldNotDeleteNote, t.common.pleaseTryAgain); } }, }, @@ -195,12 +196,12 @@ export default function LibraryScreen() { const confirmDeleteFolderKeepNotes = useCallback((folder: FolderMetadata) => { Alert.alert( - 'Delete folder?', - `"${folder.name}" will be deleted. Its notes will stay in your library.`, + t.library.deleteFolderTitle, + t.library.deleteFolderMessage(folder.name), [ - { text: 'Cancel', style: 'cancel' }, + { text: t.common.cancel, style: 'cancel' }, { - text: 'Delete Folder', + text: t.library.deleteFolderLabel, style: 'destructive', onPress: async () => { try { @@ -208,7 +209,7 @@ export default function LibraryScreen() { await refresh(); } catch (error) { if (__DEV__) console.warn('[LibraryScreen] delete folder failed', error); - Alert.alert('Could not delete folder', 'Please try again.'); + Alert.alert(t.library.couldNotDeleteFolder, t.common.pleaseTryAgain); } }, }, @@ -217,14 +218,13 @@ export default function LibraryScreen() { }, [refresh]); const confirmDeleteFolderAndNotes = useCallback((folder: FolderMetadata, noteCount: number) => { - const noteText = noteCount === 1 ? '1 note' : `${noteCount} notes`; Alert.alert( - 'Delete folder and notes?', - `"${folder.name}" and ${noteText} inside it will be permanently deleted.`, + t.library.deleteFolderAndNotesTitle, + t.library.deleteFolderAndNotesMessage(folder.name, t.library.noteCount(noteCount)), [ - { text: 'Cancel', style: 'cancel' }, + { text: t.common.cancel, style: 'cancel' }, { - text: 'Delete All', + text: t.library.deleteAllLabel, style: 'destructive', onPress: async () => { try { @@ -232,7 +232,7 @@ export default function LibraryScreen() { await refresh(); } catch (error) { if (__DEV__) console.warn('[LibraryScreen] delete folder notes failed', error); - Alert.alert('Could not delete folder', 'Please try again.'); + Alert.alert(t.library.couldNotDeleteFolder, t.common.pleaseTryAgain); } }, }, @@ -248,7 +248,7 @@ export default function LibraryScreen() { { key: 'openNotes', icon: 'heart-outline', - accessibilityLabel: 'Support OpenNotes', + accessibilityLabel: t.library.supportA11y, onPress: () => setAction({ kind: 'openNotes' }), }, ]} @@ -259,8 +259,8 @@ export default function LibraryScreen() { ) : sortedFolders.length === 0 && rootNotes.length === 0 ? ( ) : ( @@ -269,7 +269,7 @@ export default function LibraryScreen() { showsVerticalScrollIndicator={false} > {sortedFolders.length > 0 ? ( - + {sortedFolders.map((folder) => ( 0 ? ( - 0 ? 'Notes' : ''}> + 0 ? t.library.notesSection : ''}> {rootNotes.map((note) => ( setAction({ kind: 'createNoteBackground' }), }, { key: 'folder', - label: 'New Folder', + label: t.library.newFolder, onPress: handleCreateFolder, }, ]} @@ -371,17 +371,17 @@ export default function LibraryScreen() { ? [ { key: 'rename', - label: 'Rename', + label: t.common.rename, onPress: () => setAction({ kind: 'renameNote', note: action.note }), }, { key: 'move', - label: 'Move to Folder', + label: t.library.moveToFolder, onPress: () => setAction({ kind: 'moveNote', note: action.note }), }, { key: 'delete', - label: 'Delete', + label: t.common.delete, destructive: true, onPress: () => confirmDeleteNote(action.note), }, @@ -399,19 +399,19 @@ export default function LibraryScreen() { ? [ { key: 'rename', - label: 'Rename', + label: t.common.rename, onPress: () => setAction({ kind: 'renameFolder', folder: action.folder }), }, { key: 'orphan', - label: 'Delete (keep notes)', + label: t.library.deleteKeepNotesLabel, destructive: true, onPress: () => confirmDeleteFolderKeepNotes(action.folder), }, { key: 'deleteNotes', - label: 'Delete folder and all notes', + label: t.library.deleteFolderAndNotesLabel, destructive: true, onPress: () => confirmDeleteFolderAndNotes( @@ -427,9 +427,9 @@ export default function LibraryScreen() { setAction(null)} onConfirm={async (value) => { if (action?.kind === 'renameNote') { @@ -442,9 +442,9 @@ export default function LibraryScreen() { setAction(null)} onConfirm={async (value) => { if (action?.kind === 'renameFolder') { @@ -457,10 +457,10 @@ export default function LibraryScreen() { setAction(null)} onConfirm={async (value) => { if (!value.trim()) { diff --git a/ios/OpenNotes.xcodeproj/project.pbxproj b/ios/OpenNotes.xcodeproj/project.pbxproj index 6c50b37..a0fc9d0 100644 --- a/ios/OpenNotes.xcodeproj/project.pbxproj +++ b/ios/OpenNotes.xcodeproj/project.pbxproj @@ -288,6 +288,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", @@ -299,6 +300,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3026d95..14e4818 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -222,6 +222,8 @@ PODS: - ExpoModulesCore - ExpoLinking (8.0.12): - ExpoModulesCore + - ExpoLocalization (17.0.9): + - ExpoModulesCore - ExpoModulesCore (3.0.30): - hermes-engine - RCTRequired @@ -2239,6 +2241,7 @@ DEPENDENCIES: - ExpoImagePicker (from `../node_modules/expo-image-picker/ios`) - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) - ExpoLinking (from `../node_modules/expo-linking/ios`) + - ExpoLocalization (from `../node_modules/expo-localization/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - ExpoPrint (from `../node_modules/expo-print/ios`) - ExpoSharing (from `../node_modules/expo-sharing/ios`) @@ -2358,6 +2361,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-keep-awake/ios" ExpoLinking: :path: "../node_modules/expo-linking/ios" + ExpoLocalization: + :path: "../node_modules/expo-localization/ios" ExpoModulesCore: :path: "../node_modules/expo-modules-core" ExpoPrint: @@ -2539,6 +2544,7 @@ SPEC CHECKSUMS: ExpoImagePicker: ade85e148f735fc7e6f27dfe3eef28eb3887e90a ExpoKeepAwake: 55f75eca6499bb9e4231ebad6f3e9cb8f99c0296 ExpoLinking: 2ef632ad5013e3f1568fae9408d56ab9af5efc55 + ExpoLocalization: d936ff63557366e2e487264f5b495b1f0637fabd ExpoModulesCore: 9e6a5514828e7dd5ded9e99d33557be2d284e660 ExpoPrint: 813bfa965eda2ecc4b1cdc61612c820e239c382a ExpoSharing: 0d983394ed4a80334bab5a0d5384f75710feb7e8 diff --git a/package-lock.json b/package-lock.json index 2b31e28..53dc7c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "expo-haptics": "~15.0.8", "expo-image-picker": "~17.0.11", "expo-linking": "~8.0.12", + "expo-localization": "~17.0.9", "expo-print": "~15.0.8", "expo-router": "~6.0.23", "expo-sharing": "~14.0.8", @@ -3525,7 +3526,7 @@ "version": "19.1.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -4545,7 +4546,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -5011,6 +5012,19 @@ "react-native": "*" } }, + "node_modules/expo-localization": { + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-17.0.9.tgz", + "integrity": "sha512-k5eYHr7iLMob3M8cHH6bgDrDRmqnZG8xKGLhpx8ST+LsFXmSgYpJ/t0VwWm0gz64C/K669XhObdG3D6QwCGqhw==", + "license": "MIT", + "dependencies": { + "rtl-detect": "^1.0.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, "node_modules/expo-manifests": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.11.tgz", @@ -7683,26 +7697,6 @@ } } }, - "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.8" - } - }, - "node_modules/react-dom/node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -8200,6 +8194,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rtl-detect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", + "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -8921,7 +8921,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 62943d3..58859e7 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "expo-haptics": "~15.0.8", "expo-image-picker": "~17.0.11", "expo-linking": "~8.0.12", + "expo-localization": "~17.0.9", "expo-print": "~15.0.8", "expo-router": "~6.0.23", "expo-sharing": "~14.0.8", diff --git a/src/components/library/CommunityInviteSheet.tsx b/src/components/library/CommunityInviteSheet.tsx index cfd09c5..5252d44 100644 --- a/src/components/library/CommunityInviteSheet.tsx +++ b/src/components/library/CommunityInviteSheet.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '../../hooks/useTheme'; +import { t } from '../../i18n'; import { radius, spacing } from '../../theme/spacing'; import { Sheet } from '../ui/Sheet'; @@ -34,14 +35,13 @@ export function CommunityInviteSheet({ maxFontSizeMultiplier={2} style={[styles.title, { color: theme.colors.text }]} > - Help shape OpenNotes. + {t.community.title} - Share ideas, vote on what comes next, and meet people who believe notes - should stay free. + {t.community.body} - Join the community + {t.community.join} - Not now + {t.common.notNow} diff --git a/src/components/library/CreateNoteBackgroundSheet.tsx b/src/components/library/CreateNoteBackgroundSheet.tsx index 81390ef..6dee62b 100644 --- a/src/components/library/CreateNoteBackgroundSheet.tsx +++ b/src/components/library/CreateNoteBackgroundSheet.tsx @@ -11,7 +11,8 @@ import { Sheet } from '../ui/Sheet'; import { useTheme } from '../../hooks/useTheme'; import { radius, spacing } from '../../theme/spacing'; import { typography } from '../../theme/typography'; -import { BACKGROUND_LABELS, BACKGROUND_TYPES } from '../../types/note'; +import { BACKGROUND_TYPES } from '../../types/note'; +import { t } from '../../i18n'; import type { BackgroundType } from '../../types/note'; export interface CreateNoteBackgroundSheetProps { @@ -41,12 +42,12 @@ export function CreateNoteBackgroundSheet({ - New note + {t.library.newNoteSheetTitle} - {BACKGROUND_LABELS[type]} + {t.library.backgrounds[type]} ))} diff --git a/src/components/library/FolderCard.tsx b/src/components/library/FolderCard.tsx index 9398a3b..06b68c8 100644 --- a/src/components/library/FolderCard.tsx +++ b/src/components/library/FolderCard.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '../../hooks/useTheme'; +import { t } from '../../i18n'; import { radius, spacing } from '../../theme/spacing'; import { typography } from '../../theme/typography'; import type { FolderMetadata } from '../../types/note'; @@ -68,7 +69,7 @@ export function FolderCard({ folder, noteCount, onPress, onLongPress, onMenuPres style={[typography.caption, styles.meta, { color: theme.colors.textTertiary }]} numberOfLines={1} > - {noteCount === 0 ? 'Empty' : `${noteCount} note${noteCount === 1 ? '' : 's'}`} + {noteCount === 0 ? t.library.folderEmpty : t.library.noteCount(noteCount)} ); diff --git a/src/components/library/FolderPickerSheet.tsx b/src/components/library/FolderPickerSheet.tsx index c36b678..5dd95d1 100644 --- a/src/components/library/FolderPickerSheet.tsx +++ b/src/components/library/FolderPickerSheet.tsx @@ -3,6 +3,7 @@ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { Sheet } from '../ui/Sheet'; import { useTheme } from '../../hooks/useTheme'; +import { t } from '../../i18n'; import { radius, spacing } from '../../theme/spacing'; import { typography } from '../../theme/typography'; import type { FolderMetadata } from '../../types/note'; @@ -41,7 +42,7 @@ export function FolderPickerSheet({ > {allowMoveToRoot ? ( { diff --git a/src/components/library/LibraryHeader.tsx b/src/components/library/LibraryHeader.tsx index 07c778f..eb269f7 100644 --- a/src/components/library/LibraryHeader.tsx +++ b/src/components/library/LibraryHeader.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '../../hooks/useTheme'; +import { t } from '../../i18n'; import { spacing } from '../../theme/spacing'; import { typography } from '../../theme/typography'; @@ -38,7 +39,7 @@ export function LibraryHeader({ key: 'primary', icon: rightIcon, label: rightLabel, - accessibilityLabel: rightLabel ?? 'Header action', + accessibilityLabel: rightLabel ?? t.library.headerActionA11y, onPress: onRightPress, }] : []; @@ -61,7 +62,7 @@ export function LibraryHeader({ style={({ pressed }) => [styles.backButton, pressed && { opacity: 0.6 }]} > - Library + {t.library.backLabel} ) : null} diff --git a/src/components/library/NoteCard.tsx b/src/components/library/NoteCard.tsx index 0d3bcc0..c9acb9c 100644 --- a/src/components/library/NoteCard.tsx +++ b/src/components/library/NoteCard.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Image, Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '../../hooks/useTheme'; +import { t } from '../../i18n'; import { radius, spacing } from '../../theme/spacing'; import { typography } from '../../theme/typography'; import { formatRelative } from '../../utils/relativeTime'; @@ -74,7 +75,7 @@ export function NoteCard({ note, onPress, onLongPress, onMenuPress }: NoteCardPr style={[typography.footnote, styles.title, { color: theme.colors.text }]} numberOfLines={2} > - {note.title || 'Untitled'} + {note.title || t.common.untitled} - Free notes need a little help. + {t.sheet.title} - Everything here is optional. Writing notes never depends on it. + {t.sheet.subtitle} { void openExternalLink(OPEN_NOTES_LINKS.github, 'OpenNotesSheet'); }} /> @@ -104,13 +105,13 @@ export function OpenNotesSheet({ > @@ -122,19 +123,19 @@ export function OpenNotesSheet({ { void openExternalLink(OPEN_NOTES_LINKS.privacy, 'OpenNotesSheet'); }} /> { void openExternalLink(OPEN_NOTES_LINKS.terms, 'OpenNotesSheet'); }} /> { void openExternalLink(OPEN_NOTES_LINKS.support, 'OpenNotesSheet'); }} diff --git a/src/components/library/RenameDialog.tsx b/src/components/library/RenameDialog.tsx index 5975cd4..ad4e27c 100644 --- a/src/components/library/RenameDialog.tsx +++ b/src/components/library/RenameDialog.tsx @@ -10,6 +10,7 @@ import { View, } from 'react-native'; import { useTheme } from '../../hooks/useTheme'; +import { t } from '../../i18n'; import { radius, spacing } from '../../theme/spacing'; import { typography } from '../../theme/typography'; @@ -28,7 +29,7 @@ export function RenameDialog({ title, initialValue, placeholder, - confirmLabel = 'Save', + confirmLabel = t.common.save, onCancel, onConfirm, }: RenameDialogProps) { @@ -92,7 +93,7 @@ export function RenameDialog({ style={[styles.button, { backgroundColor: theme.colors.surfaceMuted }]} onPress={onCancel} > - Cancel + {t.common.cancel} {slides.map((slide, index) => ( @@ -169,7 +170,7 @@ export function OnboardingExperience({ [styles.skip, pressed && styles.pressed]} @@ -179,7 +180,7 @@ export function OnboardingExperience({ numberOfLines={1} style={[styles.skipText, { color: theme.colors.textSecondary }]} > - Skip + {t.onboarding.skip} @@ -223,10 +224,10 @@ export function OnboardingExperience({ style={styles.primaryButtonText} > {isBackupPage - ? 'Enable iCloud backup' + ? t.onboarding.enableBackup : isLastPage - ? 'Start writing' - : 'Continue'} + ? t.onboarding.startWriting + : t.common.continue} - Continue without backup + {t.onboarding.continueWithoutBackup} ) : null} diff --git a/src/hooks/useBackup.ts b/src/hooks/useBackup.ts index 7588381..7fdfef4 100644 --- a/src/hooks/useBackup.ts +++ b/src/hooks/useBackup.ts @@ -10,6 +10,7 @@ import { setBackupEnabled, } from '../services/backupService'; import { formatRelative } from '../utils/relativeTime'; +import { t } from '../i18n'; export interface UseBackupResult { /** False on platforms without a backup implementation (Android, for now). */ @@ -76,20 +77,19 @@ export function useBackup( restorePromptShownRef.current = true; const when = availability.lastBackupAt - ? ` (backed up ${formatRelative(new Date(availability.lastBackupAt).toISOString())})` - : ''; - const noun = availability.noteCount === 1 ? 'note' : 'notes'; + ? formatRelative(new Date(availability.lastBackupAt).toISOString()) + : null; Alert.alert( - 'Restore your notes?', - `An iCloud backup with ${availability.noteCount} ${noun}${when} was found for this device's account.`, + t.backup.restoreTitle, + t.backup.restoreMessage(t.library.noteCount(availability.noteCount), when), [ { - text: 'Not now', + text: t.common.notNow, style: 'cancel', onPress: () => scheduleBackupSync(), }, { - text: 'Restore', + text: t.backup.restoreAction, onPress: () => { void (async () => { const result = await performBackupRestore(); @@ -97,13 +97,13 @@ export function useBackup( await refreshLibrary(); scheduleBackupSync(); if (result.status === 'ok') { - Alert.alert('Restore complete', `${result.restored} files restored.`); + Alert.alert(t.backup.restoreCompleteTitle, t.backup.restoreCompleteMessage(result.restored)); } else { Alert.alert( - 'Restore finished with problems', + t.backup.restoreProblemsTitle, result.status === 'partial' - ? `${result.restored} files restored, ${result.failed} could not be read from iCloud. Try again later for the rest.` - : 'iCloud is not reachable right now. Try again later.', + ? t.backup.restorePartialMessage(result.restored, result.failed) + : t.backup.restoreUnavailableMessage, ); } })(); @@ -129,12 +129,12 @@ export function useBackup( return; } Alert.alert( - 'Turn off iCloud backup?', - 'Your notes will exist only on this device. Deleting the app will permanently delete them.', + t.backup.turnOffTitle, + t.backup.turnOffMessage, [ - { text: 'Keep backup on', style: 'cancel' }, + { text: t.backup.keepOn, style: 'cancel' }, { - text: 'Turn off', + text: t.backup.turnOffConfirm, style: 'destructive', onPress: () => setBackupPreference(false), }, @@ -144,10 +144,10 @@ export function useBackup( const backupSubtitle = enabled === false - ? 'Off — notes exist only on this device' + ? t.backup.statusOff : available === false - ? 'iCloud unavailable — sign in to iCloud to protect your notes' - : 'Automatic — your notes survive app deletion'; + ? t.backup.statusUnavailable + : t.backup.statusAutomatic; return { backupSupported: BACKUP_SUPPORTED, diff --git a/src/hooks/useLibrarySupport.ts b/src/hooks/useLibrarySupport.ts index b88b81d..6577458 100644 --- a/src/hooks/useLibrarySupport.ts +++ b/src/hooks/useLibrarySupport.ts @@ -2,6 +2,7 @@ import { useCallback, useRef } from 'react'; import { Alert } from 'react-native'; import { useFocusEffect } from 'expo-router'; import { OPEN_NOTES_LINKS, openExternalLink } from '../services/externalLinks'; +import { t } from '../i18n'; import { claimCommunityPrompt, requestAutomaticReviewIfEligible, @@ -71,7 +72,7 @@ export function useLibrarySupport({ onClose(); } catch (error) { if (__DEV__) console.warn('[useLibrarySupport] join failed', error); - Alert.alert('Could not update community status', 'Please try again.'); + Alert.alert(t.community.couldNotUpdateTitle, t.common.pleaseTryAgain); } finally { actionInFlightRef.current = false; } @@ -84,13 +85,13 @@ export function useLibrarySupport({ const requested = await requestManualReview(); if (!requested) { Alert.alert( - 'Ratings are not available yet', - 'The rating option becomes available when OpenNotes is installed from an app store.', + t.community.ratingsUnavailableTitle, + t.community.ratingsUnavailableMessage, ); } } catch (error) { if (__DEV__) console.warn('[useLibrarySupport] review failed', error); - Alert.alert('Could not open ratings', 'Please try again later.'); + Alert.alert(t.community.couldNotOpenRatingsTitle, t.common.pleaseTryAgainLater); } finally { actionInFlightRef.current = false; } diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..bb184d1 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,75 @@ +import { en, type Strings } from './locales/en'; +import { de } from './locales/de'; +import { fr } from './locales/fr'; +import { es } from './locales/es'; +import { it } from './locales/it'; +import { pt } from './locales/pt'; +import { ja } from './locales/ja'; +import { ko } from './locales/ko'; +import { nl } from './locales/nl'; +import { ru } from './locales/ru'; +import { zhHans } from './locales/zhHans'; +import { zhHant } from './locales/zhHant'; + +const BY_LANGUAGE: Record = { + en, + de, + fr, + es, + it, + pt, + ja, + ko, + nl, + ru, +}; + +const TRADITIONAL_REGIONS = new Set(['TW', 'HK', 'MO']); + +/** + * Resolves the string catalog once at startup from the device's preferred + * languages (a locale change on iOS restarts the app, so static resolution + * is safe). Falls back to English for unsupported languages or if the + * localization module is unavailable. + */ +interface DeviceLocale { + languageCode: string | null; + languageScriptCode?: string | null; + languageRegionCode?: string | null; + regionCode: string | null; +} + +function resolveStrings(): Strings { + let locales: DeviceLocale[]; + try { + // Required lazily inside try: importing expo-localization throws at load + // time when the native module is absent (older binary, unit tests), and + // the app must fall back to English rather than crash. + const { getLocales } = require('expo-localization') as { + getLocales: () => DeviceLocale[]; + }; + locales = getLocales(); + } catch { + return en; + } + for (const locale of locales) { + const lang = locale.languageCode; + if (!lang) continue; + if (lang === 'zh') { + // The script code is authoritative when present (iOS always sets it + // for Chinese); the region is only a fallback, and the LANGUAGE's + // region at that - regionCode is the device Region setting, which must + // not override an explicit Hans/Hant choice. + const traditional = locale.languageScriptCode + ? locale.languageScriptCode === 'Hant' + : TRADITIONAL_REGIONS.has(locale.languageRegionCode ?? locale.regionCode ?? ''); + return traditional ? zhHant : zhHans; + } + const match = BY_LANGUAGE[lang]; + if (match) return match; + } + return en; +} + +/** The active string catalog. Import `t` and read `t.section.key`. */ +export const t: Strings = resolveStrings(); diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts new file mode 100644 index 0000000..b78fdf8 --- /dev/null +++ b/src/i18n/locales/de.ts @@ -0,0 +1,151 @@ +import type { Strings } from './en'; + +export const de: Strings = { + common: { + cancel: 'Abbrechen', + delete: 'Löschen', + rename: 'Umbenennen', + save: 'Sichern', + create: 'Erstellen', + continue: 'Weiter', + notNow: 'Nicht jetzt', + untitled: 'Ohne Titel', + folder: 'Ordner', + pleaseTryAgain: 'Bitte versuche es erneut.', + pleaseTryAgainLater: 'Bitte versuche es später erneut.', + }, + library: { + backLabel: 'Bibliothek', + foldersSection: 'Ordner', + notesSection: 'Notizen', + emptyTitle: 'Noch keine Notizen', + emptySubtitle: 'Tippe auf das Plus, um deine erste Notiz oder deinen ersten Ordner zu erstellen.', + emptyFolderTitle: 'Leerer Ordner', + emptyFolderSubtitle: (folderName: string) => + `Tippe auf das Plus, um eine Notiz zu ${folderName} hinzuzufügen.`, + thisFolder: 'diesem Ordner', + newNote: 'Neue Notiz', + newFolder: 'Neuer Ordner', + createTitle: 'Erstellen', + newFolderTitle: 'Neuer Ordner', + folderNamePlaceholder: 'Ordnername', + renameNoteTitle: 'Notiz umbenennen', + renameFolderTitle: 'Ordner umbenennen', + noteTitlePlaceholder: 'Titel der Notiz', + newNoteSheetTitle: 'Neue Notiz', + titlePlaceholder: 'Titel', + moveToFolder: 'In Ordner verschieben', + moveToRoot: 'Aus Ordner entfernen', + root: 'Oberste Ebene', + folderEmpty: 'Leer', + noteCount: (count: number) => (count === 1 ? '1 Notiz' : `${count} Notizen`), + supportA11y: 'OpenNotes unterstützen', + headerActionA11y: 'Kopfzeilenaktion', + deleteNoteTitle: 'Notiz löschen?', + deleteNoteMessage: (title: string) => `„${title}“ wird endgültig gelöscht.`, + deleteFolderTitle: 'Ordner löschen?', + deleteFolderMessage: (name: string) => + `„${name}“ wird gelöscht. Die Notizen bleiben in deiner Bibliothek.`, + deleteFolderLabel: 'Ordner löschen', + deleteKeepNotesLabel: 'Löschen (Notizen behalten)', + deleteFolderAndNotesTitle: 'Ordner und Notizen löschen?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `„${name}“ und ${noteText} darin werden endgültig gelöscht.`, + deleteFolderAndNotesLabel: 'Ordner und alle Notizen löschen', + deleteAllLabel: 'Alles löschen', + couldNotCreateNote: 'Notiz konnte nicht erstellt werden', + couldNotDeleteNote: 'Notiz konnte nicht gelöscht werden', + couldNotDeleteFolder: 'Ordner konnte nicht gelöscht werden', + backgrounds: { + plain: 'Blanko', + lined: 'Liniert', + grid: 'Kariert', + dotted: 'Gepunktet', + graph: 'Millimeterpapier', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: 'Fortschritt der Einführung', + skip: 'Überspringen', + skipA11y: 'Einführung überspringen', + startWriting: 'Loslegen', + enableBackup: 'iCloud-Backup aktivieren', + continueWithoutBackup: 'Ohne Backup fortfahren', + slides: { + free: { + title: 'Notizen sollten\nfrei sein.', + body: 'OpenNotes ist es. Schreibe von Hand, markiere PDFs und behalte jede Seite — ohne Abo.', + imageLabel: 'Papier und ein Aluminium-Stift, der eine blaue Linie zeichnet', + }, + privacy: { + title: 'Und sie sollten\ndir gehören.', + body: 'Kein Konto. Kein Tracking. Deine Notizen bleiben auf deinem Gerät, bis du dich anders entscheidest.', + imageLabel: 'Eine Notiz, sicher in einer gläsernen Archivbox', + }, + mission: { + title: 'Für alle gebaut.', + body: 'OpenNotes ist kostenlos, privat und offen entwickelt. Das ist das Versprechen.', + imageLabel: 'Ein offenes Notizbuch mit drei gewebten Lesezeichen am Falz', + }, + backup: { + title: 'Sicher, selbst bei\nKatastrophen.', + body: 'Ein verlorenes iPad, ein Absturz, eine gelöschte App — mit iCloud-Backup überstehen deine Notizen alles, in deiner eigenen iCloud. Nichts verlässt jemals deinen Apple-Account.', + imageLabel: 'Eine Notiz, geschützt in einer klaren Hülle, sicher vor Schaden', + }, + }, + }, + backup: { + rowTitle: 'iCloud-Backup', + statusAutomatic: 'Automatisch — deine Notizen überstehen das Löschen der App', + statusUnavailable: 'iCloud nicht verfügbar — melde dich bei iCloud an, um deine Notizen zu schützen', + statusOff: 'Aus — Notizen existieren nur auf diesem Gerät', + turnOffTitle: 'iCloud-Backup ausschalten?', + turnOffMessage: + 'Deine Notizen existieren dann nur auf diesem Gerät. Wird die App gelöscht, sind sie endgültig verloren.', + turnOffConfirm: 'Ausschalten', + keepOn: 'Backup anlassen', + restoreTitle: 'Notizen wiederherstellen?', + restoreMessage: (noteText: string, when: string | null) => + `Ein iCloud-Backup mit ${noteText}${when ? ` (gesichert ${when})` : ''} wurde für den Account dieses Geräts gefunden.`, + restoreAction: 'Wiederherstellen', + restoreCompleteTitle: 'Wiederherstellung abgeschlossen', + restoreCompleteMessage: (count: number) => `${count} Dateien wiederhergestellt.`, + restoreProblemsTitle: 'Wiederherstellung mit Problemen beendet', + restorePartialMessage: (restored: number, failed: number) => + `${restored} Dateien wiederhergestellt, ${failed} konnten nicht aus iCloud gelesen werden. Versuche es für den Rest später erneut.`, + restoreUnavailableMessage: 'iCloud ist gerade nicht erreichbar. Versuche es später erneut.', + }, + sheet: { + title: 'Freie Notizen brauchen etwas Hilfe.', + subtitle: 'Alles hier ist freiwillig. Notizen schreiben hängt nie davon ab.', + joinTitle: 'Community beitreten', + joinSubtitle: 'Ideen, Updates und Gespräche', + rateTitle: 'OpenNotes bewerten', + rateSubtitle: 'Eine App-Store-Bewertung hinterlassen', + githubTitle: 'Auf GitHub folgen', + githubSubtitle: 'Dem Open-Source-Projekt folgen', + missionTitle: 'Unsere Mission', + missionSubtitle: 'Warum OpenNotes frei und privat bleibt', + privacy: 'Datenschutz', + terms: 'Bedingungen', + support: 'Support', + }, + community: { + title: 'Gestalte OpenNotes mit.', + body: 'Teile Ideen, stimme über die nächsten Schritte ab und triff Menschen, die finden, dass Notizen frei bleiben sollten.', + join: 'Community beitreten', + couldNotUpdateTitle: 'Community-Status konnte nicht aktualisiert werden', + ratingsUnavailableTitle: 'Bewertungen sind noch nicht verfügbar', + ratingsUnavailableMessage: + 'Die Bewertungsfunktion wird verfügbar, wenn OpenNotes aus einem App Store installiert ist.', + couldNotOpenRatingsTitle: 'Bewertungen konnten nicht geöffnet werden', + }, + time: { + justNow: 'gerade eben', + minutesAgo: (m: number) => `vor ${m} Min.`, + hoursAgo: (h: number) => `vor ${h} Std.`, + yesterday: 'gestern', + daysAgo: (d: number) => `vor ${d} T.`, + }, +}; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts new file mode 100644 index 0000000..2dfa23c --- /dev/null +++ b/src/i18n/locales/en.ts @@ -0,0 +1,153 @@ +// Master string catalog. Every other locale file is typed as `Strings`, so +// the compiler enforces that no key is ever missing from a translation. +export const en = { + common: { + cancel: 'Cancel', + delete: 'Delete', + rename: 'Rename', + save: 'Save', + create: 'Create', + continue: 'Continue', + notNow: 'Not now', + untitled: 'Untitled', + folder: 'Folder', + pleaseTryAgain: 'Please try again.', + pleaseTryAgainLater: 'Please try again later.', + }, + library: { + backLabel: 'Library', + foldersSection: 'Folders', + notesSection: 'Notes', + emptyTitle: 'No notes yet', + emptySubtitle: 'Tap the plus button to create your first note or folder.', + emptyFolderTitle: 'Empty folder', + emptyFolderSubtitle: (folderName: string) => + `Tap the plus button to add a note to ${folderName}.`, + thisFolder: 'this folder', + newNote: 'New Note', + newFolder: 'New Folder', + createTitle: 'Create', + newFolderTitle: 'New folder', + folderNamePlaceholder: 'Folder name', + renameNoteTitle: 'Rename note', + renameFolderTitle: 'Rename folder', + noteTitlePlaceholder: 'Note title', + newNoteSheetTitle: 'New note', + titlePlaceholder: 'Title', + moveToFolder: 'Move to Folder', + moveToRoot: 'Move to Root', + root: 'Root', + folderEmpty: 'Empty', + noteCount: (count: number) => (count === 1 ? '1 note' : `${count} notes`), + supportA11y: 'Support OpenNotes', + headerActionA11y: 'Header action', + deleteNoteTitle: 'Delete note?', + deleteNoteMessage: (title: string) => `"${title}" will be permanently deleted.`, + deleteFolderTitle: 'Delete folder?', + deleteFolderMessage: (name: string) => + `"${name}" will be deleted. Its notes will stay in your library.`, + deleteFolderLabel: 'Delete Folder', + deleteKeepNotesLabel: 'Delete (keep notes)', + deleteFolderAndNotesTitle: 'Delete folder and notes?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `"${name}" and ${noteText} inside it will be permanently deleted.`, + deleteFolderAndNotesLabel: 'Delete folder and all notes', + deleteAllLabel: 'Delete All', + couldNotCreateNote: 'Could not create note', + couldNotDeleteNote: 'Could not delete note', + couldNotDeleteFolder: 'Could not delete folder', + backgrounds: { + plain: 'Plain', + lined: 'Lined', + grid: 'Grid', + dotted: 'Dotted', + graph: 'Graph', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: 'Introduction progress', + skip: 'Skip', + skipA11y: 'Skip introduction', + startWriting: 'Start writing', + enableBackup: 'Enable iCloud backup', + continueWithoutBackup: 'Continue without backup', + slides: { + free: { + title: 'Notes should\nbe free.', + body: 'So OpenNotes is. Write by hand, mark up PDFs, and keep every page without a subscription.', + imageLabel: 'Paper and an aluminum stylus drawing a blue line', + }, + privacy: { + title: 'And they should\nstay yours.', + body: 'No account. No tracking. Your notes stay on your device until you decide otherwise.', + imageLabel: 'A note secured inside a glass archival case', + }, + mission: { + title: 'Built for everyone.', + body: 'OpenNotes is free, private, and built in the open. That is the promise.', + imageLabel: 'An open notebook with three woven bookmarks meeting at its binding', + }, + backup: { + title: 'Safe, even from\ndisasters.', + body: 'A lost iPad, a hard crash, a deleted app — with iCloud backup your notes survive them all, in your own iCloud. Nothing ever leaves your Apple account.', + imageLabel: 'A note protected inside a clear case, safe from harm', + }, + }, + }, + backup: { + rowTitle: 'iCloud backup', + statusAutomatic: 'Automatic — your notes survive app deletion', + statusUnavailable: 'iCloud unavailable — sign in to iCloud to protect your notes', + statusOff: 'Off — notes exist only on this device', + turnOffTitle: 'Turn off iCloud backup?', + turnOffMessage: + 'Your notes will exist only on this device. Deleting the app will permanently delete them.', + turnOffConfirm: 'Turn off', + keepOn: 'Keep backup on', + restoreTitle: 'Restore your notes?', + restoreMessage: (noteText: string, when: string | null) => + `An iCloud backup with ${noteText}${when ? ` (backed up ${when})` : ''} was found for this device's account.`, + restoreAction: 'Restore', + restoreCompleteTitle: 'Restore complete', + restoreCompleteMessage: (count: number) => `${count} files restored.`, + restoreProblemsTitle: 'Restore finished with problems', + restorePartialMessage: (restored: number, failed: number) => + `${restored} files restored, ${failed} could not be read from iCloud. Try again later for the rest.`, + restoreUnavailableMessage: 'iCloud is not reachable right now. Try again later.', + }, + sheet: { + title: 'Free notes need a little help.', + subtitle: 'Everything here is optional. Writing notes never depends on it.', + joinTitle: 'Join the community', + joinSubtitle: 'Ideas, updates, and conversations', + rateTitle: 'Rate OpenNotes', + rateSubtitle: 'Leave an App Store review', + githubTitle: 'Star on GitHub', + githubSubtitle: 'Follow the open-source project', + missionTitle: 'Our mission', + missionSubtitle: 'Why OpenNotes stays free and private', + privacy: 'Privacy', + terms: 'Terms', + support: 'Support', + }, + community: { + title: 'Help shape OpenNotes.', + body: 'Share ideas, vote on what comes next, and meet people who believe notes should stay free.', + join: 'Join the community', + couldNotUpdateTitle: 'Could not update community status', + ratingsUnavailableTitle: 'Ratings are not available yet', + ratingsUnavailableMessage: + 'The rating option becomes available when OpenNotes is installed from an app store.', + couldNotOpenRatingsTitle: 'Could not open ratings', + }, + time: { + justNow: 'just now', + minutesAgo: (m: number) => `${m}m ago`, + hoursAgo: (h: number) => `${h}h ago`, + yesterday: 'yesterday', + daysAgo: (d: number) => `${d}d ago`, + }, +}; + +export type Strings = typeof en; diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts new file mode 100644 index 0000000..f2c5f41 --- /dev/null +++ b/src/i18n/locales/es.ts @@ -0,0 +1,151 @@ +import type { Strings } from './en'; + +export const es: Strings = { + common: { + cancel: 'Cancelar', + delete: 'Eliminar', + rename: 'Renombrar', + save: 'Guardar', + create: 'Crear', + continue: 'Continuar', + notNow: 'Ahora no', + untitled: 'Sin título', + folder: 'Carpeta', + pleaseTryAgain: 'Inténtalo de nuevo.', + pleaseTryAgainLater: 'Inténtalo de nuevo más tarde.', + }, + library: { + backLabel: 'Biblioteca', + foldersSection: 'Carpetas', + notesSection: 'Notas', + emptyTitle: 'Aún no hay notas', + emptySubtitle: 'Toca el botón más para crear tu primera nota o carpeta.', + emptyFolderTitle: 'Carpeta vacía', + emptyFolderSubtitle: (folderName: string) => + `Toca el botón más para añadir una nota a ${folderName}.`, + thisFolder: 'esta carpeta', + newNote: 'Nueva nota', + newFolder: 'Nueva carpeta', + createTitle: 'Crear', + newFolderTitle: 'Nueva carpeta', + folderNamePlaceholder: 'Nombre de la carpeta', + renameNoteTitle: 'Renombrar nota', + renameFolderTitle: 'Renombrar carpeta', + noteTitlePlaceholder: 'Título de la nota', + newNoteSheetTitle: 'Nueva nota', + titlePlaceholder: 'Título', + moveToFolder: 'Mover a carpeta', + moveToRoot: 'Sacar de la carpeta', + root: 'Raíz', + folderEmpty: 'Vacía', + noteCount: (count: number) => (count === 1 ? '1 nota' : `${count} notas`), + supportA11y: 'Apoyar OpenNotes', + headerActionA11y: 'Acción de cabecera', + deleteNoteTitle: '¿Eliminar nota?', + deleteNoteMessage: (title: string) => `«${title}» se eliminará permanentemente.`, + deleteFolderTitle: '¿Eliminar carpeta?', + deleteFolderMessage: (name: string) => + `«${name}» se eliminará. Sus notas permanecerán en tu biblioteca.`, + deleteFolderLabel: 'Eliminar carpeta', + deleteKeepNotesLabel: 'Eliminar (conservar notas)', + deleteFolderAndNotesTitle: '¿Eliminar carpeta y notas?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `«${name}» y ${noteText} que contiene se eliminarán permanentemente.`, + deleteFolderAndNotesLabel: 'Eliminar carpeta y todas las notas', + deleteAllLabel: 'Eliminar todo', + couldNotCreateNote: 'No se pudo crear la nota', + couldNotDeleteNote: 'No se pudo eliminar la nota', + couldNotDeleteFolder: 'No se pudo eliminar la carpeta', + backgrounds: { + plain: 'Liso', + lined: 'Rayado', + grid: 'Cuadrícula', + dotted: 'Punteado', + graph: 'Milimetrado', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: 'Progreso de la introducción', + skip: 'Omitir', + skipA11y: 'Omitir la introducción', + startWriting: 'Empezar a escribir', + enableBackup: 'Activar copia en iCloud', + continueWithoutBackup: 'Continuar sin copia', + slides: { + free: { + title: 'Las notas deberían\nser libres.', + body: 'OpenNotes lo es. Escribe a mano, anota PDF y conserva cada página sin suscripción.', + imageLabel: 'Papel y un lápiz de aluminio trazando una línea azul', + }, + privacy: { + title: 'Y deberían seguir\nsiendo tuyas.', + body: 'Sin cuentas. Sin rastreo. Tus notas permanecen en tu dispositivo hasta que tú decidas lo contrario.', + imageLabel: 'Una nota protegida dentro de una vitrina de archivo de cristal', + }, + mission: { + title: 'Hecho para todos.', + body: 'OpenNotes es gratuito, privado y desarrollado en abierto. Esa es la promesa.', + imageLabel: 'Un cuaderno abierto con tres marcapáginas tejidos unidos en el lomo', + }, + backup: { + title: 'A salvo, incluso\nde desastres.', + body: 'Un iPad perdido, un fallo grave, una app borrada: con la copia en iCloud tus notas sobreviven a todo, en tu propio iCloud. Nada sale nunca de tu cuenta de Apple.', + imageLabel: 'Una nota protegida en una funda transparente, a salvo de daños', + }, + }, + }, + backup: { + rowTitle: 'Copia en iCloud', + statusAutomatic: 'Automática — tus notas sobreviven si borras la app', + statusUnavailable: 'iCloud no disponible — inicia sesión en iCloud para proteger tus notas', + statusOff: 'Desactivada — las notas solo existen en este dispositivo', + turnOffTitle: '¿Desactivar la copia en iCloud?', + turnOffMessage: + 'Tus notas existirán solo en este dispositivo. Si borras la app, se eliminarán permanentemente.', + turnOffConfirm: 'Desactivar', + keepOn: 'Mantener la copia', + restoreTitle: '¿Restaurar tus notas?', + restoreMessage: (noteText: string, when: string | null) => + `Se encontró una copia de iCloud con ${noteText}${when ? ` (guardada ${when})` : ''} para la cuenta de este dispositivo.`, + restoreAction: 'Restaurar', + restoreCompleteTitle: 'Restauración completada', + restoreCompleteMessage: (count: number) => `${count} archivos restaurados.`, + restoreProblemsTitle: 'Restauración terminada con problemas', + restorePartialMessage: (restored: number, failed: number) => + `${restored} archivos restaurados; ${failed} no se pudieron leer de iCloud. Inténtalo más tarde para el resto.`, + restoreUnavailableMessage: 'iCloud no está disponible ahora mismo. Inténtalo más tarde.', + }, + sheet: { + title: 'Las notas libres necesitan una pequeña ayuda.', + subtitle: 'Todo lo de aquí es opcional. Escribir notas nunca depende de ello.', + joinTitle: 'Unirse a la comunidad', + joinSubtitle: 'Ideas, novedades y conversaciones', + rateTitle: 'Valorar OpenNotes', + rateSubtitle: 'Deja una reseña en el App Store', + githubTitle: 'Dar una estrella en GitHub', + githubSubtitle: 'Sigue el proyecto de código abierto', + missionTitle: 'Nuestra misión', + missionSubtitle: 'Por qué OpenNotes sigue siendo gratuito y privado', + privacy: 'Privacidad', + terms: 'Términos', + support: 'Soporte', + }, + community: { + title: 'Ayuda a dar forma a OpenNotes.', + body: 'Comparte ideas, vota lo próximo y conoce a gente convencida de que las notas deben seguir siendo libres.', + join: 'Unirse a la comunidad', + couldNotUpdateTitle: 'No se pudo actualizar el estado de la comunidad', + ratingsUnavailableTitle: 'Las valoraciones aún no están disponibles', + ratingsUnavailableMessage: + 'La opción de valorar estará disponible cuando OpenNotes se instale desde una tienda de apps.', + couldNotOpenRatingsTitle: 'No se pudieron abrir las valoraciones', + }, + time: { + justNow: 'ahora mismo', + minutesAgo: (m: number) => `hace ${m} min`, + hoursAgo: (h: number) => `hace ${h} h`, + yesterday: 'ayer', + daysAgo: (d: number) => `hace ${d} d`, + }, +}; diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts new file mode 100644 index 0000000..187d0c6 --- /dev/null +++ b/src/i18n/locales/fr.ts @@ -0,0 +1,151 @@ +import type { Strings } from './en'; + +export const fr: Strings = { + common: { + cancel: 'Annuler', + delete: 'Supprimer', + rename: 'Renommer', + save: 'Enregistrer', + create: 'Créer', + continue: 'Continuer', + notNow: 'Plus tard', + untitled: 'Sans titre', + folder: 'Dossier', + pleaseTryAgain: 'Veuillez réessayer.', + pleaseTryAgainLater: 'Veuillez réessayer plus tard.', + }, + library: { + backLabel: 'Bibliothèque', + foldersSection: 'Dossiers', + notesSection: 'Notes', + emptyTitle: 'Aucune note pour l’instant', + emptySubtitle: 'Touchez le bouton plus pour créer votre première note ou votre premier dossier.', + emptyFolderTitle: 'Dossier vide', + emptyFolderSubtitle: (folderName: string) => + `Touchez le bouton plus pour ajouter une note à ${folderName}.`, + thisFolder: 'ce dossier', + newNote: 'Nouvelle note', + newFolder: 'Nouveau dossier', + createTitle: 'Créer', + newFolderTitle: 'Nouveau dossier', + folderNamePlaceholder: 'Nom du dossier', + renameNoteTitle: 'Renommer la note', + renameFolderTitle: 'Renommer le dossier', + noteTitlePlaceholder: 'Titre de la note', + newNoteSheetTitle: 'Nouvelle note', + titlePlaceholder: 'Titre', + moveToFolder: 'Déplacer vers un dossier', + moveToRoot: 'Sortir du dossier', + root: 'Racine', + folderEmpty: 'Vide', + noteCount: (count: number) => (count === 1 ? '1 note' : `${count} notes`), + supportA11y: 'Soutenir OpenNotes', + headerActionA11y: 'Action d’en-tête', + deleteNoteTitle: 'Supprimer la note ?', + deleteNoteMessage: (title: string) => `« ${title} » sera définitivement supprimée.`, + deleteFolderTitle: 'Supprimer le dossier ?', + deleteFolderMessage: (name: string) => + `« ${name} » sera supprimé. Ses notes resteront dans votre bibliothèque.`, + deleteFolderLabel: 'Supprimer le dossier', + deleteKeepNotesLabel: 'Supprimer (garder les notes)', + deleteFolderAndNotesTitle: 'Supprimer le dossier et les notes ?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `« ${name} » et ${noteText} qu’il contient seront définitivement supprimés.`, + deleteFolderAndNotesLabel: 'Supprimer le dossier et toutes les notes', + deleteAllLabel: 'Tout supprimer', + couldNotCreateNote: 'Impossible de créer la note', + couldNotDeleteNote: 'Impossible de supprimer la note', + couldNotDeleteFolder: 'Impossible de supprimer le dossier', + backgrounds: { + plain: 'Uni', + lined: 'Ligné', + grid: 'Quadrillé', + dotted: 'Pointillé', + graph: 'Millimétré', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: 'Progression de l’introduction', + skip: 'Passer', + skipA11y: 'Passer l’introduction', + startWriting: 'Commencer à écrire', + enableBackup: 'Activer la sauvegarde iCloud', + continueWithoutBackup: 'Continuer sans sauvegarde', + slides: { + free: { + title: 'Les notes devraient\nêtre libres.', + body: 'OpenNotes l’est. Écrivez à la main, annotez des PDF et gardez chaque page, sans abonnement.', + imageLabel: 'Du papier et un stylet en aluminium traçant une ligne bleue', + }, + privacy: { + title: 'Et elles devraient\nrester à vous.', + body: 'Pas de compte. Pas de pistage. Vos notes restent sur votre appareil tant que vous n’en décidez pas autrement.', + imageLabel: 'Une note protégée dans une vitrine d’archivage en verre', + }, + mission: { + title: 'Conçu pour tous.', + body: 'OpenNotes est gratuit, privé et développé en toute transparence. C’est la promesse.', + imageLabel: 'Un carnet ouvert avec trois marque-pages tissés réunis à la reliure', + }, + backup: { + title: 'À l’abri, même\ndes catastrophes.', + body: 'Un iPad perdu, une panne, une app supprimée : avec la sauvegarde iCloud, vos notes survivent à tout, dans votre propre iCloud. Rien ne quitte jamais votre compte Apple.', + imageLabel: 'Une note protégée dans un étui transparent, à l’abri de tout', + }, + }, + }, + backup: { + rowTitle: 'Sauvegarde iCloud', + statusAutomatic: 'Automatique — vos notes survivent à la suppression de l’app', + statusUnavailable: 'iCloud indisponible — connectez-vous à iCloud pour protéger vos notes', + statusOff: 'Désactivée — les notes n’existent que sur cet appareil', + turnOffTitle: 'Désactiver la sauvegarde iCloud ?', + turnOffMessage: + 'Vos notes n’existeront que sur cet appareil. Supprimer l’app les supprimera définitivement.', + turnOffConfirm: 'Désactiver', + keepOn: 'Garder la sauvegarde', + restoreTitle: 'Restaurer vos notes ?', + restoreMessage: (noteText: string, when: string | null) => + `Une sauvegarde iCloud avec ${noteText}${when ? ` (sauvegardée ${when})` : ''} a été trouvée pour le compte de cet appareil.`, + restoreAction: 'Restaurer', + restoreCompleteTitle: 'Restauration terminée', + restoreCompleteMessage: (count: number) => `${count} fichiers restaurés.`, + restoreProblemsTitle: 'Restauration terminée avec des problèmes', + restorePartialMessage: (restored: number, failed: number) => + `${restored} fichiers restaurés, ${failed} n’ont pas pu être lus depuis iCloud. Réessayez plus tard pour le reste.`, + restoreUnavailableMessage: 'iCloud est momentanément inaccessible. Réessayez plus tard.', + }, + sheet: { + title: 'Les notes libres ont besoin d’un coup de main.', + subtitle: 'Tout ici est facultatif. Écrire des notes n’en dépend jamais.', + joinTitle: 'Rejoindre la communauté', + joinSubtitle: 'Idées, nouveautés et discussions', + rateTitle: 'Évaluer OpenNotes', + rateSubtitle: 'Laisser un avis sur l’App Store', + githubTitle: 'Ajouter une étoile sur GitHub', + githubSubtitle: 'Suivre le projet open source', + missionTitle: 'Notre mission', + missionSubtitle: 'Pourquoi OpenNotes reste gratuit et privé', + privacy: 'Confidentialité', + terms: 'Conditions', + support: 'Assistance', + }, + community: { + title: 'Façonnez OpenNotes.', + body: 'Partagez des idées, votez pour la suite et rencontrez des personnes convaincues que les notes doivent rester libres.', + join: 'Rejoindre la communauté', + couldNotUpdateTitle: 'Impossible de mettre à jour le statut communauté', + ratingsUnavailableTitle: 'Les évaluations ne sont pas encore disponibles', + ratingsUnavailableMessage: + 'L’évaluation devient disponible lorsque OpenNotes est installé depuis un app store.', + couldNotOpenRatingsTitle: 'Impossible d’ouvrir les évaluations', + }, + time: { + justNow: 'à l’instant', + minutesAgo: (m: number) => `il y a ${m} min`, + hoursAgo: (h: number) => `il y a ${h} h`, + yesterday: 'hier', + daysAgo: (d: number) => `il y a ${d} j`, + }, +}; diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts new file mode 100644 index 0000000..e3edc6e --- /dev/null +++ b/src/i18n/locales/it.ts @@ -0,0 +1,151 @@ +import type { Strings } from './en'; + +export const it: Strings = { + common: { + cancel: 'Annulla', + delete: 'Elimina', + rename: 'Rinomina', + save: 'Salva', + create: 'Crea', + continue: 'Continua', + notNow: 'Non ora', + untitled: 'Senza titolo', + folder: 'Cartella', + pleaseTryAgain: 'Riprova.', + pleaseTryAgainLater: 'Riprova più tardi.', + }, + library: { + backLabel: 'Libreria', + foldersSection: 'Cartelle', + notesSection: 'Note', + emptyTitle: 'Ancora nessuna nota', + emptySubtitle: 'Tocca il pulsante più per creare la tua prima nota o cartella.', + emptyFolderTitle: 'Cartella vuota', + emptyFolderSubtitle: (folderName: string) => + `Tocca il pulsante più per aggiungere una nota a ${folderName}.`, + thisFolder: 'questa cartella', + newNote: 'Nuova nota', + newFolder: 'Nuova cartella', + createTitle: 'Crea', + newFolderTitle: 'Nuova cartella', + folderNamePlaceholder: 'Nome della cartella', + renameNoteTitle: 'Rinomina nota', + renameFolderTitle: 'Rinomina cartella', + noteTitlePlaceholder: 'Titolo della nota', + newNoteSheetTitle: 'Nuova nota', + titlePlaceholder: 'Titolo', + moveToFolder: 'Sposta in una cartella', + moveToRoot: 'Togli dalla cartella', + root: 'Radice', + folderEmpty: 'Vuota', + noteCount: (count: number) => (count === 1 ? '1 nota' : `${count} note`), + supportA11y: 'Sostieni OpenNotes', + headerActionA11y: 'Azione dell’intestazione', + deleteNoteTitle: 'Eliminare la nota?', + deleteNoteMessage: (title: string) => `«${title}» verrà eliminata definitivamente.`, + deleteFolderTitle: 'Eliminare la cartella?', + deleteFolderMessage: (name: string) => + `«${name}» verrà eliminata. Le sue note resteranno nella tua libreria.`, + deleteFolderLabel: 'Elimina cartella', + deleteKeepNotesLabel: 'Elimina (conserva le note)', + deleteFolderAndNotesTitle: 'Eliminare cartella e note?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `«${name}» e ${noteText} al suo interno verranno eliminate definitivamente.`, + deleteFolderAndNotesLabel: 'Elimina la cartella e tutte le note', + deleteAllLabel: 'Elimina tutto', + couldNotCreateNote: 'Impossibile creare la nota', + couldNotDeleteNote: 'Impossibile eliminare la nota', + couldNotDeleteFolder: 'Impossibile eliminare la cartella', + backgrounds: { + plain: 'Bianco', + lined: 'A righe', + grid: 'A quadretti', + dotted: 'A puntini', + graph: 'Millimetrato', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: 'Avanzamento dell’introduzione', + skip: 'Salta', + skipA11y: 'Salta l’introduzione', + startWriting: 'Inizia a scrivere', + enableBackup: 'Attiva backup iCloud', + continueWithoutBackup: 'Continua senza backup', + slides: { + free: { + title: 'Le note dovrebbero\nessere libere.', + body: 'OpenNotes lo è. Scrivi a mano, annota i PDF e conserva ogni pagina, senza abbonamento.', + imageLabel: 'Carta e uno stilo in alluminio che traccia una linea blu', + }, + privacy: { + title: 'E dovrebbero\nrestare tue.', + body: 'Nessun account. Nessun tracciamento. Le tue note restano sul tuo dispositivo finché non decidi diversamente.', + imageLabel: 'Una nota custodita in una teca d’archivio di vetro', + }, + mission: { + title: 'Fatto per tutti.', + body: 'OpenNotes è gratuito, privato e sviluppato in modo aperto. Questa è la promessa.', + imageLabel: 'Un quaderno aperto con tre segnalibri intrecciati uniti alla rilegatura', + }, + backup: { + title: 'Al sicuro, anche\ndai disastri.', + body: 'Un iPad perso, un blocco improvviso, un’app eliminata: con il backup iCloud le tue note sopravvivono a tutto, nel tuo iCloud personale. Nulla lascia mai il tuo account Apple.', + imageLabel: 'Una nota protetta in una custodia trasparente, al riparo dai danni', + }, + }, + }, + backup: { + rowTitle: 'Backup iCloud', + statusAutomatic: 'Automatico — le tue note sopravvivono all’eliminazione dell’app', + statusUnavailable: 'iCloud non disponibile — accedi a iCloud per proteggere le tue note', + statusOff: 'Disattivato — le note esistono solo su questo dispositivo', + turnOffTitle: 'Disattivare il backup iCloud?', + turnOffMessage: + 'Le tue note esisteranno solo su questo dispositivo. Eliminando l’app verranno eliminate definitivamente.', + turnOffConfirm: 'Disattiva', + keepOn: 'Mantieni il backup', + restoreTitle: 'Ripristinare le tue note?', + restoreMessage: (noteText: string, when: string | null) => + `È stato trovato un backup iCloud con ${noteText}${when ? ` (salvato ${when})` : ''} per l’account di questo dispositivo.`, + restoreAction: 'Ripristina', + restoreCompleteTitle: 'Ripristino completato', + restoreCompleteMessage: (count: number) => `${count} file ripristinati.`, + restoreProblemsTitle: 'Ripristino concluso con problemi', + restorePartialMessage: (restored: number, failed: number) => + `${restored} file ripristinati, ${failed} non è stato possibile leggerli da iCloud. Riprova più tardi per il resto.`, + restoreUnavailableMessage: 'iCloud non è raggiungibile al momento. Riprova più tardi.', + }, + sheet: { + title: 'Le note libere hanno bisogno di una mano.', + subtitle: 'Tutto qui è facoltativo. Scrivere note non ne dipende mai.', + joinTitle: 'Unisciti alla community', + joinSubtitle: 'Idee, novità e conversazioni', + rateTitle: 'Valuta OpenNotes', + rateSubtitle: 'Lascia una recensione sull’App Store', + githubTitle: 'Metti una stella su GitHub', + githubSubtitle: 'Segui il progetto open source', + missionTitle: 'La nostra missione', + missionSubtitle: 'Perché OpenNotes resta gratuito e privato', + privacy: 'Privacy', + terms: 'Termini', + support: 'Supporto', + }, + community: { + title: 'Aiuta a plasmare OpenNotes.', + body: 'Condividi idee, vota le prossime novità e incontra chi crede che le note debbano restare libere.', + join: 'Unisciti alla community', + couldNotUpdateTitle: 'Impossibile aggiornare lo stato della community', + ratingsUnavailableTitle: 'Le valutazioni non sono ancora disponibili', + ratingsUnavailableMessage: + 'L’opzione di valutazione sarà disponibile quando OpenNotes è installato da un app store.', + couldNotOpenRatingsTitle: 'Impossibile aprire le valutazioni', + }, + time: { + justNow: 'adesso', + minutesAgo: (m: number) => `${m} min fa`, + hoursAgo: (h: number) => `${h} h fa`, + yesterday: 'ieri', + daysAgo: (d: number) => `${d} g fa`, + }, +}; diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts new file mode 100644 index 0000000..3fab4a9 --- /dev/null +++ b/src/i18n/locales/ja.ts @@ -0,0 +1,151 @@ +import type { Strings } from './en'; + +export const ja: Strings = { + common: { + cancel: 'キャンセル', + delete: '削除', + rename: '名称変更', + save: '保存', + create: '作成', + continue: '続ける', + notNow: '今はしない', + untitled: '無題', + folder: 'フォルダ', + pleaseTryAgain: 'もう一度お試しください。', + pleaseTryAgainLater: 'しばらくしてからもう一度お試しください。', + }, + library: { + backLabel: 'ライブラリ', + foldersSection: 'フォルダ', + notesSection: 'ノート', + emptyTitle: 'まだノートがありません', + emptySubtitle: 'プラスボタンをタップして、最初のノートまたはフォルダを作成しましょう。', + emptyFolderTitle: '空のフォルダ', + emptyFolderSubtitle: (folderName: string) => + `プラスボタンをタップして、${folderName}にノートを追加しましょう。`, + thisFolder: 'このフォルダ', + newNote: '新規ノート', + newFolder: '新規フォルダ', + createTitle: '作成', + newFolderTitle: '新規フォルダ', + folderNamePlaceholder: 'フォルダ名', + renameNoteTitle: 'ノート名を変更', + renameFolderTitle: 'フォルダ名を変更', + noteTitlePlaceholder: 'ノートのタイトル', + newNoteSheetTitle: '新規ノート', + titlePlaceholder: 'タイトル', + moveToFolder: 'フォルダへ移動', + moveToRoot: 'フォルダから出す', + root: 'ルート', + folderEmpty: '空', + noteCount: (count: number) => `${count}件のノート`, + supportA11y: 'OpenNotesを応援する', + headerActionA11y: 'ヘッダー操作', + deleteNoteTitle: 'ノートを削除しますか?', + deleteNoteMessage: (title: string) => `「${title}」は完全に削除されます。`, + deleteFolderTitle: 'フォルダを削除しますか?', + deleteFolderMessage: (name: string) => + `「${name}」を削除します。中のノートはライブラリに残ります。`, + deleteFolderLabel: 'フォルダを削除', + deleteKeepNotesLabel: '削除(ノートは残す)', + deleteFolderAndNotesTitle: 'フォルダとノートを削除しますか?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `「${name}」と中の${noteText}は完全に削除されます。`, + deleteFolderAndNotesLabel: 'フォルダとすべてのノートを削除', + deleteAllLabel: 'すべて削除', + couldNotCreateNote: 'ノートを作成できませんでした', + couldNotDeleteNote: 'ノートを削除できませんでした', + couldNotDeleteFolder: 'フォルダを削除できませんでした', + backgrounds: { + plain: '無地', + lined: '罫線', + grid: '方眼', + dotted: 'ドット', + graph: 'グラフ用紙', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: '説明の進行状況', + skip: 'スキップ', + skipA11y: '説明をスキップ', + startWriting: '書きはじめる', + enableBackup: 'iCloudバックアップを有効にする', + continueWithoutBackup: 'バックアップなしで続ける', + slides: { + free: { + title: 'ノートは自由で\nあるべき。', + body: 'OpenNotesはそうです。手書きで書き、PDFに書き込み、すべてのページをサブスクなしで残せます。', + imageLabel: '紙と、青い線を描くアルミのスタイラス', + }, + privacy: { + title: 'そして、あなたの\nものであるべき。', + body: 'アカウント不要。トラッキングなし。あなたが決めない限り、ノートは端末の中にとどまります。', + imageLabel: 'ガラスの保管ケースに収められたノート', + }, + mission: { + title: 'すべての人のために。', + body: 'OpenNotesは無料で、プライベートで、オープンに開発されています。それが約束です。', + imageLabel: '綴じ目に3本の織りしおりが集まる開いたノート', + }, + backup: { + title: '災難があっても、\n安全に。', + body: 'iPadの紛失、突然のクラッシュ、アプリの削除——iCloudバックアップがあれば、ノートはあなた自身のiCloudの中ですべてを生き延びます。Appleアカウントの外に出ることは決してありません。', + imageLabel: '透明なケースに守られ、損傷から安全なノート', + }, + }, + }, + backup: { + rowTitle: 'iCloudバックアップ', + statusAutomatic: '自動 — アプリを削除してもノートは残ります', + statusUnavailable: 'iCloudを利用できません — iCloudにサインインしてノートを守りましょう', + statusOff: 'オフ — ノートはこの端末にのみ存在します', + turnOffTitle: 'iCloudバックアップをオフにしますか?', + turnOffMessage: + 'ノートはこの端末にのみ存在することになります。アプリを削除するとノートは完全に削除されます。', + turnOffConfirm: 'オフにする', + keepOn: 'バックアップを続ける', + restoreTitle: 'ノートを復元しますか?', + restoreMessage: (noteText: string, when: string | null) => + `この端末のアカウントに、${noteText}を含むiCloudバックアップ${when ? `(${when}に保存)` : ''}が見つかりました。`, + restoreAction: '復元', + restoreCompleteTitle: '復元が完了しました', + restoreCompleteMessage: (count: number) => `${count}個のファイルを復元しました。`, + restoreProblemsTitle: '復元は完了しましたが問題があります', + restorePartialMessage: (restored: number, failed: number) => + `${restored}個のファイルを復元しました。${failed}個はiCloudから読み込めませんでした。残りは後でもう一度お試しください。`, + restoreUnavailableMessage: '現在iCloudに接続できません。後でもう一度お試しください。', + }, + sheet: { + title: '自由なノートには、少しの支えが必要です。', + subtitle: 'ここにあるものはすべて任意です。ノートを書くことには一切影響しません。', + joinTitle: 'コミュニティに参加', + joinSubtitle: 'アイデア、最新情報、交流', + rateTitle: 'OpenNotesを評価', + rateSubtitle: 'App Storeにレビューを書く', + githubTitle: 'GitHubでスターを付ける', + githubSubtitle: 'オープンソースプロジェクトをフォロー', + missionTitle: '私たちのミッション', + missionSubtitle: 'OpenNotesが無料でプライベートであり続ける理由', + privacy: 'プライバシー', + terms: '利用規約', + support: 'サポート', + }, + community: { + title: 'OpenNotesを一緒に育てましょう。', + body: 'アイデアを共有し、次の機能に投票し、ノートは自由であるべきだと考える仲間と出会えます。', + join: 'コミュニティに参加', + couldNotUpdateTitle: 'コミュニティの状態を更新できませんでした', + ratingsUnavailableTitle: '評価はまだ利用できません', + ratingsUnavailableMessage: + '評価機能は、OpenNotesをApp Storeからインストールすると利用できるようになります。', + couldNotOpenRatingsTitle: '評価を開けませんでした', + }, + time: { + justNow: 'たった今', + minutesAgo: (m: number) => `${m}分前`, + hoursAgo: (h: number) => `${h}時間前`, + yesterday: '昨日', + daysAgo: (d: number) => `${d}日前`, + }, +}; diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts new file mode 100644 index 0000000..3501fb9 --- /dev/null +++ b/src/i18n/locales/ko.ts @@ -0,0 +1,151 @@ +import type { Strings } from './en'; + +export const ko: Strings = { + common: { + cancel: '취소', + delete: '삭제', + rename: '이름 변경', + save: '저장', + create: '만들기', + continue: '계속', + notNow: '나중에', + untitled: '제목 없음', + folder: '폴더', + pleaseTryAgain: '다시 시도해 주세요.', + pleaseTryAgainLater: '잠시 후 다시 시도해 주세요.', + }, + library: { + backLabel: '보관함', + foldersSection: '폴더', + notesSection: '노트', + emptyTitle: '아직 노트가 없습니다', + emptySubtitle: '더하기 버튼을 눌러 첫 노트나 폴더를 만들어 보세요.', + emptyFolderTitle: '빈 폴더', + emptyFolderSubtitle: (folderName: string) => + `더하기 버튼을 눌러 ${folderName}에 노트를 추가해 보세요.`, + thisFolder: '이 폴더', + newNote: '새 노트', + newFolder: '새 폴더', + createTitle: '만들기', + newFolderTitle: '새 폴더', + folderNamePlaceholder: '폴더 이름', + renameNoteTitle: '노트 이름 변경', + renameFolderTitle: '폴더 이름 변경', + noteTitlePlaceholder: '노트 제목', + newNoteSheetTitle: '새 노트', + titlePlaceholder: '제목', + moveToFolder: '폴더로 이동', + moveToRoot: '폴더에서 꺼내기', + root: '최상위', + folderEmpty: '비어 있음', + noteCount: (count: number) => `노트 ${count}개`, + supportA11y: 'OpenNotes 응원하기', + headerActionA11y: '헤더 동작', + deleteNoteTitle: '노트를 삭제할까요?', + deleteNoteMessage: (title: string) => `"${title}"이(가) 영구히 삭제됩니다.`, + deleteFolderTitle: '폴더를 삭제할까요?', + deleteFolderMessage: (name: string) => + `"${name}"이(가) 삭제됩니다. 안의 노트는 보관함에 남습니다.`, + deleteFolderLabel: '폴더 삭제', + deleteKeepNotesLabel: '삭제(노트 유지)', + deleteFolderAndNotesTitle: '폴더와 노트를 삭제할까요?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `"${name}"과(와) 안에 있는 ${noteText}가 영구히 삭제됩니다.`, + deleteFolderAndNotesLabel: '폴더와 모든 노트 삭제', + deleteAllLabel: '모두 삭제', + couldNotCreateNote: '노트를 만들지 못했습니다', + couldNotDeleteNote: '노트를 삭제하지 못했습니다', + couldNotDeleteFolder: '폴더를 삭제하지 못했습니다', + backgrounds: { + plain: '무지', + lined: '줄노트', + grid: '모눈', + dotted: '점', + graph: '그래프 용지', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: '소개 진행 상황', + skip: '건너뛰기', + skipA11y: '소개 건너뛰기', + startWriting: '쓰기 시작', + enableBackup: 'iCloud 백업 켜기', + continueWithoutBackup: '백업 없이 계속', + slides: { + free: { + title: '노트는 자유로워야\n합니다.', + body: 'OpenNotes가 그렇습니다. 손으로 쓰고, PDF에 표시하고, 구독 없이 모든 페이지를 간직하세요.', + imageLabel: '종이와 파란 선을 긋는 알루미늄 스타일러스', + }, + privacy: { + title: '그리고 당신의 것으로\n남아야 합니다.', + body: '계정 없음. 추적 없음. 당신이 달리 정하기 전까지 노트는 기기에 머뭅니다.', + imageLabel: '유리 보관함 안에 안전하게 담긴 노트', + }, + mission: { + title: '모두를 위해 만들었습니다.', + body: 'OpenNotes는 무료이고, 프라이빗하며, 공개적으로 개발됩니다. 그것이 약속입니다.', + imageLabel: '제본부에 세 개의 짜임 책갈피가 모인 펼쳐진 노트', + }, + backup: { + title: '재난이 와도\n안전하게.', + body: '잃어버린 iPad, 갑작스러운 고장, 삭제된 앱 — iCloud 백업이 있으면 노트는 당신의 iCloud 안에서 모두 살아남습니다. 어떤 것도 당신의 Apple 계정을 떠나지 않습니다.', + imageLabel: '투명한 케이스에 보호되어 손상으로부터 안전한 노트', + }, + }, + }, + backup: { + rowTitle: 'iCloud 백업', + statusAutomatic: '자동 — 앱을 삭제해도 노트가 남습니다', + statusUnavailable: 'iCloud 사용 불가 — iCloud에 로그인해 노트를 보호하세요', + statusOff: '꺼짐 — 노트가 이 기기에만 존재합니다', + turnOffTitle: 'iCloud 백업을 끌까요?', + turnOffMessage: + '노트가 이 기기에만 존재하게 됩니다. 앱을 삭제하면 노트도 영구히 삭제됩니다.', + turnOffConfirm: '끄기', + keepOn: '백업 유지', + restoreTitle: '노트를 복원할까요?', + restoreMessage: (noteText: string, when: string | null) => + `이 기기의 계정에서 ${noteText}가 담긴 iCloud 백업${when ? ` (${when} 백업)` : ''}을 찾았습니다.`, + restoreAction: '복원', + restoreCompleteTitle: '복원 완료', + restoreCompleteMessage: (count: number) => `파일 ${count}개를 복원했습니다.`, + restoreProblemsTitle: '복원이 완료되었지만 문제가 있습니다', + restorePartialMessage: (restored: number, failed: number) => + `파일 ${restored}개를 복원했고, ${failed}개는 iCloud에서 읽지 못했습니다. 나머지는 나중에 다시 시도해 주세요.`, + restoreUnavailableMessage: '지금은 iCloud에 연결할 수 없습니다. 나중에 다시 시도해 주세요.', + }, + sheet: { + title: '자유로운 노트에는 작은 도움이 필요합니다.', + subtitle: '여기 있는 것은 모두 선택 사항입니다. 노트 쓰기는 이것에 좌우되지 않습니다.', + joinTitle: '커뮤니티 참여', + joinSubtitle: '아이디어, 소식, 대화', + rateTitle: 'OpenNotes 평가하기', + rateSubtitle: 'App Store에 리뷰 남기기', + githubTitle: 'GitHub에서 스타 누르기', + githubSubtitle: '오픈소스 프로젝트 팔로우', + missionTitle: '우리의 미션', + missionSubtitle: 'OpenNotes가 무료이며 프라이빗한 이유', + privacy: '개인정보 보호', + terms: '이용약관', + support: '지원', + }, + community: { + title: 'OpenNotes를 함께 만들어요.', + body: '아이디어를 나누고, 다음 기능에 투표하고, 노트는 자유로워야 한다고 믿는 사람들을 만나 보세요.', + join: '커뮤니티 참여', + couldNotUpdateTitle: '커뮤니티 상태를 업데이트하지 못했습니다', + ratingsUnavailableTitle: '아직 평가를 사용할 수 없습니다', + ratingsUnavailableMessage: + '평가 기능은 OpenNotes를 앱 스토어에서 설치했을 때 사용할 수 있습니다.', + couldNotOpenRatingsTitle: '평가를 열지 못했습니다', + }, + time: { + justNow: '방금', + minutesAgo: (m: number) => `${m}분 전`, + hoursAgo: (h: number) => `${h}시간 전`, + yesterday: '어제', + daysAgo: (d: number) => `${d}일 전`, + }, +}; diff --git a/src/i18n/locales/nl.ts b/src/i18n/locales/nl.ts new file mode 100644 index 0000000..b345208 --- /dev/null +++ b/src/i18n/locales/nl.ts @@ -0,0 +1,151 @@ +import type { Strings } from './en'; + +export const nl: Strings = { + common: { + cancel: 'Annuleer', + delete: 'Verwijder', + rename: 'Wijzig naam', + save: 'Bewaar', + create: 'Maak aan', + continue: 'Ga door', + notNow: 'Niet nu', + untitled: 'Naamloos', + folder: 'Map', + pleaseTryAgain: 'Probeer het opnieuw.', + pleaseTryAgainLater: 'Probeer het later opnieuw.', + }, + library: { + backLabel: 'Bibliotheek', + foldersSection: 'Mappen', + notesSection: 'Notities', + emptyTitle: 'Nog geen notities', + emptySubtitle: 'Tik op de plusknop om je eerste notitie of map te maken.', + emptyFolderTitle: 'Lege map', + emptyFolderSubtitle: (folderName: string) => + `Tik op de plusknop om een notitie toe te voegen aan ${folderName}.`, + thisFolder: 'deze map', + newNote: 'Nieuwe notitie', + newFolder: 'Nieuwe map', + createTitle: 'Maak aan', + newFolderTitle: 'Nieuwe map', + folderNamePlaceholder: 'Mapnaam', + renameNoteTitle: 'Notitie hernoemen', + renameFolderTitle: 'Map hernoemen', + noteTitlePlaceholder: 'Titel van notitie', + newNoteSheetTitle: 'Nieuwe notitie', + titlePlaceholder: 'Titel', + moveToFolder: 'Verplaats naar map', + moveToRoot: 'Haal uit map', + root: 'Hoofdniveau', + folderEmpty: 'Leeg', + noteCount: (count: number) => (count === 1 ? '1 notitie' : `${count} notities`), + supportA11y: 'Steun OpenNotes', + headerActionA11y: 'Kopactie', + deleteNoteTitle: 'Notitie verwijderen?', + deleteNoteMessage: (title: string) => `'${title}' wordt definitief verwijderd.`, + deleteFolderTitle: 'Map verwijderen?', + deleteFolderMessage: (name: string) => + `'${name}' wordt verwijderd. De notities blijven in je bibliotheek.`, + deleteFolderLabel: 'Verwijder map', + deleteKeepNotesLabel: 'Verwijder (behoud notities)', + deleteFolderAndNotesTitle: 'Map en notities verwijderen?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `'${name}' en de ${noteText} erin worden definitief verwijderd.`, + deleteFolderAndNotesLabel: 'Verwijder map en alle notities', + deleteAllLabel: 'Verwijder alles', + couldNotCreateNote: 'Kon notitie niet aanmaken', + couldNotDeleteNote: 'Kon notitie niet verwijderen', + couldNotDeleteFolder: 'Kon map niet verwijderen', + backgrounds: { + plain: 'Blanco', + lined: 'Gelinieerd', + grid: 'Geruit', + dotted: 'Gestippeld', + graph: 'Millimeter', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: 'Voortgang van de introductie', + skip: 'Sla over', + skipA11y: 'Introductie overslaan', + startWriting: 'Begin met schrijven', + enableBackup: 'Zet iCloud-back-up aan', + continueWithoutBackup: 'Ga door zonder back-up', + slides: { + free: { + title: 'Notities horen\nvrij te zijn.', + body: 'OpenNotes is dat. Schrijf met de hand, markeer PDF’s en bewaar elke pagina, zonder abonnement.', + imageLabel: 'Papier en een aluminium stylus die een blauwe lijn tekent', + }, + privacy: { + title: 'En ze horen van\njou te blijven.', + body: 'Geen account. Geen tracking. Je notities blijven op je apparaat tot jij anders beslist.', + imageLabel: 'Een notitie veilig in een glazen archiefvitrine', + }, + mission: { + title: 'Gemaakt voor iedereen.', + body: 'OpenNotes is gratis, privé en open ontwikkeld. Dat is de belofte.', + imageLabel: 'Een open notitieboek met drie geweven boekenleggers bij de rug', + }, + backup: { + title: 'Veilig, zelfs bij\nrampen.', + body: 'Een verloren iPad, een crash, een verwijderde app — met iCloud-back-up overleven je notities alles, in je eigen iCloud. Er verlaat nooit iets je Apple-account.', + imageLabel: 'Een notitie beschermd in een doorzichtige hoes, veilig voor schade', + }, + }, + }, + backup: { + rowTitle: 'iCloud-back-up', + statusAutomatic: 'Automatisch — je notities overleven het verwijderen van de app', + statusUnavailable: 'iCloud niet beschikbaar — log in bij iCloud om je notities te beschermen', + statusOff: 'Uit — notities bestaan alleen op dit apparaat', + turnOffTitle: 'iCloud-back-up uitzetten?', + turnOffMessage: + 'Je notities bestaan dan alleen op dit apparaat. Als je de app verwijdert, worden ze definitief verwijderd.', + turnOffConfirm: 'Zet uit', + keepOn: 'Houd back-up aan', + restoreTitle: 'Je notities terugzetten?', + restoreMessage: (noteText: string, when: string | null) => + `Er is een iCloud-back-up met ${noteText}${when ? ` (gemaakt ${when})` : ''} gevonden voor het account van dit apparaat.`, + restoreAction: 'Zet terug', + restoreCompleteTitle: 'Terugzetten voltooid', + restoreCompleteMessage: (count: number) => `${count} bestanden teruggezet.`, + restoreProblemsTitle: 'Terugzetten voltooid met problemen', + restorePartialMessage: (restored: number, failed: number) => + `${restored} bestanden teruggezet, ${failed} konden niet uit iCloud worden gelezen. Probeer het later opnieuw voor de rest.`, + restoreUnavailableMessage: 'iCloud is momenteel niet bereikbaar. Probeer het later opnieuw.', + }, + sheet: { + title: 'Vrije notities hebben een beetje hulp nodig.', + subtitle: 'Alles hier is optioneel. Notities schrijven hangt er nooit van af.', + joinTitle: 'Word lid van de community', + joinSubtitle: 'Ideeën, updates en gesprekken', + rateTitle: 'Beoordeel OpenNotes', + rateSubtitle: 'Laat een App Store-recensie achter', + githubTitle: 'Geef een ster op GitHub', + githubSubtitle: 'Volg het opensourceproject', + missionTitle: 'Onze missie', + missionSubtitle: 'Waarom OpenNotes gratis en privé blijft', + privacy: 'Privacy', + terms: 'Voorwaarden', + support: 'Ondersteuning', + }, + community: { + title: 'Help OpenNotes vorm te geven.', + body: 'Deel ideeën, stem over wat er komt en ontmoet mensen die vinden dat notities vrij moeten blijven.', + join: 'Word lid van de community', + couldNotUpdateTitle: 'Kon communitystatus niet bijwerken', + ratingsUnavailableTitle: 'Beoordelingen zijn nog niet beschikbaar', + ratingsUnavailableMessage: + 'Beoordelen wordt beschikbaar wanneer OpenNotes via een appwinkel is geïnstalleerd.', + couldNotOpenRatingsTitle: 'Kon beoordelingen niet openen', + }, + time: { + justNow: 'zojuist', + minutesAgo: (m: number) => `${m} min geleden`, + hoursAgo: (h: number) => `${h} u geleden`, + yesterday: 'gisteren', + daysAgo: (d: number) => `${d} d geleden`, + }, +}; diff --git a/src/i18n/locales/pt.ts b/src/i18n/locales/pt.ts new file mode 100644 index 0000000..e1ca58b --- /dev/null +++ b/src/i18n/locales/pt.ts @@ -0,0 +1,151 @@ +import type { Strings } from './en'; + +export const pt: Strings = { + common: { + cancel: 'Cancelar', + delete: 'Apagar', + rename: 'Renomear', + save: 'Salvar', + create: 'Criar', + continue: 'Continuar', + notNow: 'Agora não', + untitled: 'Sem título', + folder: 'Pasta', + pleaseTryAgain: 'Tente novamente.', + pleaseTryAgainLater: 'Tente novamente mais tarde.', + }, + library: { + backLabel: 'Biblioteca', + foldersSection: 'Pastas', + notesSection: 'Notas', + emptyTitle: 'Nenhuma nota ainda', + emptySubtitle: 'Toque no botão de mais para criar sua primeira nota ou pasta.', + emptyFolderTitle: 'Pasta vazia', + emptyFolderSubtitle: (folderName: string) => + `Toque no botão de mais para adicionar uma nota a ${folderName}.`, + thisFolder: 'esta pasta', + newNote: 'Nova nota', + newFolder: 'Nova pasta', + createTitle: 'Criar', + newFolderTitle: 'Nova pasta', + folderNamePlaceholder: 'Nome da pasta', + renameNoteTitle: 'Renomear nota', + renameFolderTitle: 'Renomear pasta', + noteTitlePlaceholder: 'Título da nota', + newNoteSheetTitle: 'Nova nota', + titlePlaceholder: 'Título', + moveToFolder: 'Mover para pasta', + moveToRoot: 'Tirar da pasta', + root: 'Raiz', + folderEmpty: 'Vazia', + noteCount: (count: number) => (count === 1 ? '1 nota' : `${count} notas`), + supportA11y: 'Apoiar o OpenNotes', + headerActionA11y: 'Ação do cabeçalho', + deleteNoteTitle: 'Apagar nota?', + deleteNoteMessage: (title: string) => `"${title}" será apagada permanentemente.`, + deleteFolderTitle: 'Apagar pasta?', + deleteFolderMessage: (name: string) => + `"${name}" será apagada. As notas dela permanecerão na sua biblioteca.`, + deleteFolderLabel: 'Apagar pasta', + deleteKeepNotesLabel: 'Apagar (manter notas)', + deleteFolderAndNotesTitle: 'Apagar pasta e notas?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `"${name}" e ${noteText} dentro dela serão apagadas permanentemente.`, + deleteFolderAndNotesLabel: 'Apagar a pasta e todas as notas', + deleteAllLabel: 'Apagar tudo', + couldNotCreateNote: 'Não foi possível criar a nota', + couldNotDeleteNote: 'Não foi possível apagar a nota', + couldNotDeleteFolder: 'Não foi possível apagar a pasta', + backgrounds: { + plain: 'Liso', + lined: 'Pautado', + grid: 'Quadriculado', + dotted: 'Pontilhado', + graph: 'Milimetrado', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: 'Progresso da introdução', + skip: 'Pular', + skipA11y: 'Pular introdução', + startWriting: 'Começar a escrever', + enableBackup: 'Ativar backup no iCloud', + continueWithoutBackup: 'Continuar sem backup', + slides: { + free: { + title: 'Notas deveriam\nser livres.', + body: 'O OpenNotes é. Escreva à mão, marque PDFs e guarde cada página, sem assinatura.', + imageLabel: 'Papel e uma caneta de alumínio traçando uma linha azul', + }, + privacy: { + title: 'E deveriam continuar\nsendo suas.', + body: 'Sem conta. Sem rastreamento. Suas notas ficam no seu aparelho até você decidir o contrário.', + imageLabel: 'Uma nota guardada em uma redoma de arquivo de vidro', + }, + mission: { + title: 'Feito para todos.', + body: 'O OpenNotes é gratuito, privado e desenvolvido abertamente. Essa é a promessa.', + imageLabel: 'Um caderno aberto com três marcadores trançados unidos na encadernação', + }, + backup: { + title: 'Seguro, até\nem desastres.', + body: 'Um iPad perdido, uma pane, um app apagado — com o backup no iCloud suas notas sobrevivem a tudo, no seu próprio iCloud. Nada sai da sua conta Apple.', + imageLabel: 'Uma nota protegida em um estojo transparente, a salvo de danos', + }, + }, + }, + backup: { + rowTitle: 'Backup no iCloud', + statusAutomatic: 'Automático — suas notas sobrevivem se o app for apagado', + statusUnavailable: 'iCloud indisponível — entre no iCloud para proteger suas notas', + statusOff: 'Desativado — as notas existem apenas neste aparelho', + turnOffTitle: 'Desativar o backup no iCloud?', + turnOffMessage: + 'Suas notas existirão apenas neste aparelho. Apagar o app vai apagá-las permanentemente.', + turnOffConfirm: 'Desativar', + keepOn: 'Manter o backup', + restoreTitle: 'Restaurar suas notas?', + restoreMessage: (noteText: string, when: string | null) => + `Um backup do iCloud com ${noteText}${when ? ` (feito ${when})` : ''} foi encontrado para a conta deste aparelho.`, + restoreAction: 'Restaurar', + restoreCompleteTitle: 'Restauração concluída', + restoreCompleteMessage: (count: number) => `${count} arquivos restaurados.`, + restoreProblemsTitle: 'Restauração concluída com problemas', + restorePartialMessage: (restored: number, failed: number) => + `${restored} arquivos restaurados; ${failed} não puderam ser lidos do iCloud. Tente novamente mais tarde para o restante.`, + restoreUnavailableMessage: 'O iCloud está inacessível no momento. Tente novamente mais tarde.', + }, + sheet: { + title: 'Notas livres precisam de uma ajudinha.', + subtitle: 'Tudo aqui é opcional. Escrever notas nunca depende disso.', + joinTitle: 'Entrar na comunidade', + joinSubtitle: 'Ideias, novidades e conversas', + rateTitle: 'Avaliar o OpenNotes', + rateSubtitle: 'Deixe uma avaliação na App Store', + githubTitle: 'Dar uma estrela no GitHub', + githubSubtitle: 'Acompanhe o projeto de código aberto', + missionTitle: 'Nossa missão', + missionSubtitle: 'Por que o OpenNotes continua gratuito e privado', + privacy: 'Privacidade', + terms: 'Termos', + support: 'Suporte', + }, + community: { + title: 'Ajude a moldar o OpenNotes.', + body: 'Compartilhe ideias, vote no que vem a seguir e conheça pessoas que acreditam que notas devem continuar livres.', + join: 'Entrar na comunidade', + couldNotUpdateTitle: 'Não foi possível atualizar o status da comunidade', + ratingsUnavailableTitle: 'As avaliações ainda não estão disponíveis', + ratingsUnavailableMessage: + 'A opção de avaliar fica disponível quando o OpenNotes é instalado por uma loja de apps.', + couldNotOpenRatingsTitle: 'Não foi possível abrir as avaliações', + }, + time: { + justNow: 'agora mesmo', + minutesAgo: (m: number) => `${m} min atrás`, + hoursAgo: (h: number) => `${h} h atrás`, + yesterday: 'ontem', + daysAgo: (d: number) => `${d} d atrás`, + }, +}; diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts new file mode 100644 index 0000000..7aca720 --- /dev/null +++ b/src/i18n/locales/ru.ts @@ -0,0 +1,161 @@ +import type { Strings } from './en'; + +function ruPlural(n: number, one: string, few: string, many: string): string { + const mod10 = n % 10; + const mod100 = n % 100; + if (mod10 === 1 && mod100 !== 11) return one; + if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return few; + return many; +} + +export const ru: Strings = { + common: { + cancel: 'Отменить', + delete: 'Удалить', + rename: 'Переименовать', + save: 'Сохранить', + create: 'Создать', + continue: 'Продолжить', + notNow: 'Не сейчас', + untitled: 'Без названия', + folder: 'Папка', + pleaseTryAgain: 'Попробуйте ещё раз.', + pleaseTryAgainLater: 'Попробуйте ещё раз позже.', + }, + library: { + backLabel: 'Библиотека', + foldersSection: 'Папки', + notesSection: 'Заметки', + emptyTitle: 'Пока нет заметок', + emptySubtitle: 'Нажмите кнопку с плюсом, чтобы создать первую заметку или папку.', + emptyFolderTitle: 'Пустая папка', + emptyFolderSubtitle: (folderName: string) => + `Нажмите кнопку с плюсом, чтобы добавить заметку в ${folderName}.`, + thisFolder: 'эту папку', + newNote: 'Новая заметка', + newFolder: 'Новая папка', + createTitle: 'Создать', + newFolderTitle: 'Новая папка', + folderNamePlaceholder: 'Название папки', + renameNoteTitle: 'Переименовать заметку', + renameFolderTitle: 'Переименовать папку', + noteTitlePlaceholder: 'Название заметки', + newNoteSheetTitle: 'Новая заметка', + titlePlaceholder: 'Название', + moveToFolder: 'Переместить в папку', + moveToRoot: 'Убрать из папки', + root: 'Корень', + folderEmpty: 'Пусто', + noteCount: (count: number) => + `${count} ${ruPlural(count, 'заметка', 'заметки', 'заметок')}`, + supportA11y: 'Поддержать OpenNotes', + headerActionA11y: 'Действие в шапке', + deleteNoteTitle: 'Удалить заметку?', + deleteNoteMessage: (title: string) => `«${title}» будет удалена навсегда.`, + deleteFolderTitle: 'Удалить папку?', + deleteFolderMessage: (name: string) => + `«${name}» будет удалена. Её заметки останутся в библиотеке.`, + deleteFolderLabel: 'Удалить папку', + deleteKeepNotesLabel: 'Удалить (оставить заметки)', + deleteFolderAndNotesTitle: 'Удалить папку и заметки?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `«${name}» и ${noteText} внутри будут удалены навсегда.`, + deleteFolderAndNotesLabel: 'Удалить папку и все заметки', + deleteAllLabel: 'Удалить всё', + couldNotCreateNote: 'Не удалось создать заметку', + couldNotDeleteNote: 'Не удалось удалить заметку', + couldNotDeleteFolder: 'Не удалось удалить папку', + backgrounds: { + plain: 'Чистый лист', + lined: 'В линейку', + grid: 'В клетку', + dotted: 'В точку', + graph: 'Миллиметровка', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: 'Прогресс знакомства', + skip: 'Пропустить', + skipA11y: 'Пропустить знакомство', + startWriting: 'Начать писать', + enableBackup: 'Включить копию в iCloud', + continueWithoutBackup: 'Продолжить без копии', + slides: { + free: { + title: 'Заметки должны\nбыть свободными.', + body: 'OpenNotes — именно такой. Пишите от руки, размечайте PDF и храните каждую страницу без подписки.', + imageLabel: 'Бумага и алюминиевый стилус, рисующий синюю линию', + }, + privacy: { + title: 'И должны\nоставаться вашими.', + body: 'Без аккаунта. Без слежки. Заметки остаются на вашем устройстве, пока вы не решите иначе.', + imageLabel: 'Заметка, надёжно укрытая в стеклянном архивном футляре', + }, + mission: { + title: 'Создано для всех.', + body: 'OpenNotes бесплатен, приватен и разрабатывается открыто. Это наше обещание.', + imageLabel: 'Раскрытый блокнот с тремя плетёными закладками у переплёта', + }, + backup: { + title: 'В безопасности даже\nпри катастрофах.', + body: 'Потерянный iPad, сбой, удалённое приложение — с копией в iCloud ваши заметки переживут всё, в вашем собственном iCloud. Ничто никогда не покидает ваш аккаунт Apple.', + imageLabel: 'Заметка в прозрачном футляре, защищённая от повреждений', + }, + }, + }, + backup: { + rowTitle: 'Копия в iCloud', + statusAutomatic: 'Автоматически — заметки переживут удаление приложения', + statusUnavailable: 'iCloud недоступен — войдите в iCloud, чтобы защитить заметки', + statusOff: 'Выключено — заметки существуют только на этом устройстве', + turnOffTitle: 'Выключить копию в iCloud?', + turnOffMessage: + 'Заметки будут существовать только на этом устройстве. Удаление приложения удалит их навсегда.', + turnOffConfirm: 'Выключить', + keepOn: 'Оставить включённой', + restoreTitle: 'Восстановить заметки?', + restoreMessage: (noteText: string, when: string | null) => + `Для аккаунта этого устройства найдена копия iCloud: ${noteText}${when ? ` (сохранена ${when})` : ''}.`, + restoreAction: 'Восстановить', + restoreCompleteTitle: 'Восстановление завершено', + restoreCompleteMessage: (count: number) => + `Восстановлено файлов: ${count}.`, + restoreProblemsTitle: 'Восстановление завершено с ошибками', + restorePartialMessage: (restored: number, failed: number) => + `Восстановлено файлов: ${restored}; не удалось прочитать из iCloud: ${failed}. Остальное попробуйте позже.`, + restoreUnavailableMessage: 'iCloud сейчас недоступен. Попробуйте позже.', + }, + sheet: { + title: 'Свободным заметкам нужна небольшая помощь.', + subtitle: 'Всё здесь необязательно. Ведение заметок от этого не зависит.', + joinTitle: 'Присоединиться к сообществу', + joinSubtitle: 'Идеи, новости и общение', + rateTitle: 'Оценить OpenNotes', + rateSubtitle: 'Оставить отзыв в App Store', + githubTitle: 'Поставить звезду на GitHub', + githubSubtitle: 'Следить за открытым проектом', + missionTitle: 'Наша миссия', + missionSubtitle: 'Почему OpenNotes остаётся бесплатным и приватным', + privacy: 'Конфиденциальность', + terms: 'Условия', + support: 'Поддержка', + }, + community: { + title: 'Помогите развивать OpenNotes.', + body: 'Делитесь идеями, голосуйте за будущие функции и знакомьтесь с теми, кто верит, что заметки должны оставаться свободными.', + join: 'Присоединиться к сообществу', + couldNotUpdateTitle: 'Не удалось обновить статус сообщества', + ratingsUnavailableTitle: 'Оценки пока недоступны', + ratingsUnavailableMessage: + 'Оценка станет доступна, когда OpenNotes установлен из магазина приложений.', + couldNotOpenRatingsTitle: 'Не удалось открыть оценки', + }, + time: { + justNow: 'только что', + minutesAgo: (m: number) => `${m} мин назад`, + hoursAgo: (h: number) => `${h} ч назад`, + yesterday: 'вчера', + daysAgo: (d: number) => `${d} дн. назад`, + }, +}; diff --git a/src/i18n/locales/zhHans.ts b/src/i18n/locales/zhHans.ts new file mode 100644 index 0000000..28541e9 --- /dev/null +++ b/src/i18n/locales/zhHans.ts @@ -0,0 +1,149 @@ +import type { Strings } from './en'; + +export const zhHans: Strings = { + common: { + cancel: '取消', + delete: '删除', + rename: '重命名', + save: '保存', + create: '创建', + continue: '继续', + notNow: '暂不', + untitled: '未命名', + folder: '文件夹', + pleaseTryAgain: '请重试。', + pleaseTryAgainLater: '请稍后重试。', + }, + library: { + backLabel: '资料库', + foldersSection: '文件夹', + notesSection: '笔记', + emptyTitle: '还没有笔记', + emptySubtitle: '点按加号按钮,创建你的第一条笔记或文件夹。', + emptyFolderTitle: '空文件夹', + emptyFolderSubtitle: (folderName: string) => + `点按加号按钮,向${folderName}添加笔记。`, + thisFolder: '此文件夹', + newNote: '新建笔记', + newFolder: '新建文件夹', + createTitle: '创建', + newFolderTitle: '新建文件夹', + folderNamePlaceholder: '文件夹名称', + renameNoteTitle: '重命名笔记', + renameFolderTitle: '重命名文件夹', + noteTitlePlaceholder: '笔记标题', + newNoteSheetTitle: '新建笔记', + titlePlaceholder: '标题', + moveToFolder: '移入文件夹', + moveToRoot: '移出文件夹', + root: '根目录', + folderEmpty: '空', + noteCount: (count: number) => `${count} 条笔记`, + supportA11y: '支持 OpenNotes', + headerActionA11y: '标题栏操作', + deleteNoteTitle: '删除笔记?', + deleteNoteMessage: (title: string) => `“${title}”将被永久删除。`, + deleteFolderTitle: '删除文件夹?', + deleteFolderMessage: (name: string) => + `“${name}”将被删除。其中的笔记会保留在资料库中。`, + deleteFolderLabel: '删除文件夹', + deleteKeepNotesLabel: '删除(保留笔记)', + deleteFolderAndNotesTitle: '删除文件夹和笔记?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `“${name}”及其中的${noteText}将被永久删除。`, + deleteFolderAndNotesLabel: '删除文件夹和所有笔记', + deleteAllLabel: '全部删除', + couldNotCreateNote: '无法创建笔记', + couldNotDeleteNote: '无法删除笔记', + couldNotDeleteFolder: '无法删除文件夹', + backgrounds: { + plain: '空白', + lined: '横线', + grid: '方格', + dotted: '点阵', + graph: '坐标纸', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: '介绍进度', + skip: '跳过', + skipA11y: '跳过介绍', + startWriting: '开始书写', + enableBackup: '开启 iCloud 备份', + continueWithoutBackup: '不备份,继续', + slides: { + free: { + title: '笔记应当\n是自由的。', + body: 'OpenNotes 正是如此。手写记录、批注 PDF、留住每一页,无需订阅。', + imageLabel: '纸张和一支正在画蓝线的铝制触控笔', + }, + privacy: { + title: '也应当\n属于你。', + body: '无账户。无追踪。在你另作决定之前,笔记只留在你的设备上。', + imageLabel: '安放在玻璃档案盒中的一页笔记', + }, + mission: { + title: '为每个人而做。', + body: 'OpenNotes 免费、私密、公开开发。这是我们的承诺。', + imageLabel: '摊开的笔记本,三条织带书签汇于装订处', + }, + backup: { + title: '即使遭遇灾难,\n也安然无恙。', + body: 'iPad 丢失、设备死机、应用被删——有了 iCloud 备份,你的笔记都能在你自己的 iCloud 中幸存。任何内容都不会离开你的 Apple 账户。', + imageLabel: '被透明保护壳守护、免受损伤的一页笔记', + }, + }, + }, + backup: { + rowTitle: 'iCloud 备份', + statusAutomatic: '自动 — 即使删除应用,笔记也会保留', + statusUnavailable: 'iCloud 不可用 — 请登录 iCloud 以保护你的笔记', + statusOff: '已关闭 — 笔记仅存在于此设备', + turnOffTitle: '关闭 iCloud 备份?', + turnOffMessage: '你的笔记将仅存在于此设备。删除应用会永久删除这些笔记。', + turnOffConfirm: '关闭', + keepOn: '保持备份开启', + restoreTitle: '恢复你的笔记?', + restoreMessage: (noteText: string, when: string | null) => + `在此设备的账户中找到了包含${noteText}的 iCloud 备份${when ? `(${when}备份)` : ''}。`, + restoreAction: '恢复', + restoreCompleteTitle: '恢复完成', + restoreCompleteMessage: (count: number) => `已恢复 ${count} 个文件。`, + restoreProblemsTitle: '恢复完成,但存在问题', + restorePartialMessage: (restored: number, failed: number) => + `已恢复 ${restored} 个文件,${failed} 个无法从 iCloud 读取。其余请稍后重试。`, + restoreUnavailableMessage: '目前无法连接 iCloud。请稍后重试。', + }, + sheet: { + title: '自由的笔记,需要一点支持。', + subtitle: '这里的一切都是可选的。写笔记从不依赖于此。', + joinTitle: '加入社区', + joinSubtitle: '想法、动态与交流', + rateTitle: '评价 OpenNotes', + rateSubtitle: '在 App Store 留下评价', + githubTitle: '在 GitHub 加星', + githubSubtitle: '关注这个开源项目', + missionTitle: '我们的使命', + missionSubtitle: '为什么 OpenNotes 始终免费且私密', + privacy: '隐私', + terms: '条款', + support: '支持', + }, + community: { + title: '一起塑造 OpenNotes。', + body: '分享想法、投票决定下一步,结识同样相信笔记应当自由的人。', + join: '加入社区', + couldNotUpdateTitle: '无法更新社区状态', + ratingsUnavailableTitle: '评价功能暂不可用', + ratingsUnavailableMessage: '当 OpenNotes 从应用商店安装后,评价功能即可使用。', + couldNotOpenRatingsTitle: '无法打开评价', + }, + time: { + justNow: '刚刚', + minutesAgo: (m: number) => `${m} 分钟前`, + hoursAgo: (h: number) => `${h} 小时前`, + yesterday: '昨天', + daysAgo: (d: number) => `${d} 天前`, + }, +}; diff --git a/src/i18n/locales/zhHant.ts b/src/i18n/locales/zhHant.ts new file mode 100644 index 0000000..af0b367 --- /dev/null +++ b/src/i18n/locales/zhHant.ts @@ -0,0 +1,149 @@ +import type { Strings } from './en'; + +export const zhHant: Strings = { + common: { + cancel: '取消', + delete: '刪除', + rename: '重新命名', + save: '儲存', + create: '建立', + continue: '繼續', + notNow: '暫時不要', + untitled: '未命名', + folder: '資料夾', + pleaseTryAgain: '請再試一次。', + pleaseTryAgainLater: '請稍後再試。', + }, + library: { + backLabel: '資料庫', + foldersSection: '資料夾', + notesSection: '筆記', + emptyTitle: '還沒有筆記', + emptySubtitle: '點一下加號按鈕,建立你的第一則筆記或資料夾。', + emptyFolderTitle: '空資料夾', + emptyFolderSubtitle: (folderName: string) => + `點一下加號按鈕,將筆記加入${folderName}。`, + thisFolder: '此資料夾', + newNote: '新增筆記', + newFolder: '新增資料夾', + createTitle: '建立', + newFolderTitle: '新增資料夾', + folderNamePlaceholder: '資料夾名稱', + renameNoteTitle: '重新命名筆記', + renameFolderTitle: '重新命名資料夾', + noteTitlePlaceholder: '筆記標題', + newNoteSheetTitle: '新增筆記', + titlePlaceholder: '標題', + moveToFolder: '移至資料夾', + moveToRoot: '移出資料夾', + root: '最上層', + folderEmpty: '空的', + noteCount: (count: number) => `${count} 則筆記`, + supportA11y: '支持 OpenNotes', + headerActionA11y: '標題列動作', + deleteNoteTitle: '刪除筆記?', + deleteNoteMessage: (title: string) => `「${title}」將被永久刪除。`, + deleteFolderTitle: '刪除資料夾?', + deleteFolderMessage: (name: string) => + `「${name}」將被刪除。其中的筆記會留在你的資料庫。`, + deleteFolderLabel: '刪除資料夾', + deleteKeepNotesLabel: '刪除(保留筆記)', + deleteFolderAndNotesTitle: '刪除資料夾和筆記?', + deleteFolderAndNotesMessage: (name: string, noteText: string) => + `「${name}」及其中的${noteText}將被永久刪除。`, + deleteFolderAndNotesLabel: '刪除資料夾和所有筆記', + deleteAllLabel: '全部刪除', + couldNotCreateNote: '無法建立筆記', + couldNotDeleteNote: '無法刪除筆記', + couldNotDeleteFolder: '無法刪除資料夾', + backgrounds: { + plain: '空白', + lined: '橫線', + grid: '方格', + dotted: '點陣', + graph: '方眼紙', + pdf: 'PDF', + }, + }, + onboarding: { + progressA11y: '介紹進度', + skip: '略過', + skipA11y: '略過介紹', + startWriting: '開始書寫', + enableBackup: '開啟 iCloud 備份', + continueWithoutBackup: '不備份,繼續', + slides: { + free: { + title: '筆記應該\n是自由的。', + body: 'OpenNotes 正是如此。手寫記錄、標註 PDF、留下每一頁,毋須訂閱。', + imageLabel: '紙張和一支正在畫藍線的鋁製觸控筆', + }, + privacy: { + title: '也應該\n屬於你。', + body: '沒有帳號。沒有追蹤。在你另做決定之前,筆記只留在你的裝置上。', + imageLabel: '安放於玻璃典藏盒中的一頁筆記', + }, + mission: { + title: '為每個人而造。', + body: 'OpenNotes 免費、私密、公開開發。這是我們的承諾。', + imageLabel: '攤開的筆記本,三條織帶書籤匯聚於裝訂處', + }, + backup: { + title: '即使遇上災難,\n也安然無恙。', + body: 'iPad 遺失、裝置當機、App 被刪——有了 iCloud 備份,你的筆記都能在你自己的 iCloud 中留存。任何內容都不會離開你的 Apple 帳號。', + imageLabel: '受透明保護殼守護、免於損傷的一頁筆記', + }, + }, + }, + backup: { + rowTitle: 'iCloud 備份', + statusAutomatic: '自動 — 即使刪除 App,筆記也會保留', + statusUnavailable: 'iCloud 無法使用 — 請登入 iCloud 以保護你的筆記', + statusOff: '已關閉 — 筆記僅存在於此裝置', + turnOffTitle: '關閉 iCloud 備份?', + turnOffMessage: '你的筆記將僅存在於此裝置。刪除 App 會永久刪除這些筆記。', + turnOffConfirm: '關閉', + keepOn: '保持備份開啟', + restoreTitle: '回復你的筆記?', + restoreMessage: (noteText: string, when: string | null) => + `在此裝置的帳號中找到了包含${noteText}的 iCloud 備份${when ? `(${when}備份)` : ''}。`, + restoreAction: '回復', + restoreCompleteTitle: '回復完成', + restoreCompleteMessage: (count: number) => `已回復 ${count} 個檔案。`, + restoreProblemsTitle: '回復完成,但有問題', + restorePartialMessage: (restored: number, failed: number) => + `已回復 ${restored} 個檔案,${failed} 個無法從 iCloud 讀取。其餘請稍後再試。`, + restoreUnavailableMessage: '目前無法連線 iCloud。請稍後再試。', + }, + sheet: { + title: '自由的筆記,需要一點支持。', + subtitle: '這裡的一切都是選擇性的。寫筆記從不倚賴這些。', + joinTitle: '加入社群', + joinSubtitle: '想法、消息與交流', + rateTitle: '評分 OpenNotes', + rateSubtitle: '在 App Store 留下評論', + githubTitle: '在 GitHub 加星', + githubSubtitle: '追蹤這個開源專案', + missionTitle: '我們的使命', + missionSubtitle: '為什麼 OpenNotes 始終免費且私密', + privacy: '隱私權', + terms: '條款', + support: '支援', + }, + community: { + title: '一起形塑 OpenNotes。', + body: '分享想法、投票決定下一步,認識同樣相信筆記應該自由的人。', + join: '加入社群', + couldNotUpdateTitle: '無法更新社群狀態', + ratingsUnavailableTitle: '評分功能尚無法使用', + ratingsUnavailableMessage: '當 OpenNotes 從 App 商店安裝後,即可使用評分功能。', + couldNotOpenRatingsTitle: '無法開啟評分', + }, + time: { + justNow: '剛剛', + minutesAgo: (m: number) => `${m} 分鐘前`, + hoursAgo: (h: number) => `${h} 小時前`, + yesterday: '昨天', + daysAgo: (d: number) => `${d} 天前`, + }, +}; diff --git a/src/utils/relativeTime.ts b/src/utils/relativeTime.ts index e1bce48..f244678 100644 --- a/src/utils/relativeTime.ts +++ b/src/utils/relativeTime.ts @@ -1,3 +1,5 @@ +import { t } from '../i18n'; + const SECOND = 1000; const MINUTE = 60 * SECOND; const HOUR = 60 * MINUTE; @@ -10,18 +12,16 @@ export function formatRelative(iso: string): string { const now = Date.now(); const diff = now - then; - if (diff < 30 * SECOND) return 'just now'; + if (diff < 30 * SECOND) return t.time.justNow; if (diff < HOUR) { - const m = Math.max(1, Math.floor(diff / MINUTE)); - return `${m}m ago`; + return t.time.minutesAgo(Math.max(1, Math.floor(diff / MINUTE))); } if (diff < DAY) { - const h = Math.floor(diff / HOUR); - return `${h}h ago`; + return t.time.hoursAgo(Math.floor(diff / HOUR)); } if (diff < WEEK) { const d = Math.floor(diff / DAY); - return d === 1 ? 'yesterday' : `${d}d ago`; + return d === 1 ? t.time.yesterday : t.time.daysAgo(d); } const date = new Date(iso);