You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	All: Support for plural translations (#9033)
This commit is contained in:
		| @@ -955,6 +955,7 @@ packages/renderer/noteStyle.js | ||||
| packages/renderer/pathUtils.js | ||||
| packages/renderer/utils.js | ||||
| packages/tools/build-release-stats.js | ||||
| packages/tools/build-translation.js | ||||
| packages/tools/build-welcome.js | ||||
| packages/tools/buildServerDocker.test.js | ||||
| packages/tools/buildServerDocker.js | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -941,6 +941,7 @@ packages/renderer/noteStyle.js | ||||
| packages/renderer/pathUtils.js | ||||
| packages/renderer/utils.js | ||||
| packages/tools/build-release-stats.js | ||||
| packages/tools/build-translation.js | ||||
| packages/tools/build-welcome.js | ||||
| packages/tools/buildServerDocker.test.js | ||||
| packages/tools/buildServerDocker.js | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { closestSupportedLocale } from './locale'; | ||||
| import { closestSupportedLocale, parsePluralForm, setLocale, _n } from './locale'; | ||||
|  | ||||
| describe('locale', () => { | ||||
|  | ||||
| @@ -15,4 +15,80 @@ describe('locale', () => { | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	it('should translate plurals - en_GB', () => { | ||||
| 		setLocale('en_GB'); | ||||
| 		expect(_n('Copy Shareable Link', 'Copy Shareable Links', 1)).toBe('Copy Shareable Link'); | ||||
| 		expect(_n('Copy Shareable Link', 'Copy Shareable Links', 2)).toBe('Copy Shareable Links'); | ||||
| 	}); | ||||
|  | ||||
| 	it('should translate plurals - fr_FR', () => { | ||||
| 		setLocale('fr_FR'); | ||||
| 		expect(_n('Copy Shareable Link', 'Copy Shareable Links', 1)).toBe('Copier lien partageable'); | ||||
| 		expect(_n('Copy Shareable Link', 'Copy Shareable Links', 2)).toBe('Copier liens partageables'); | ||||
| 	}); | ||||
|  | ||||
| 	it('should translate plurals - pl_PL', () => { | ||||
| 		setLocale('pl_PL'); | ||||
| 		// Not the best test since 5 is the same as 2, but it's all I could find | ||||
| 		expect(_n('Copy Shareable Link', 'Copy Shareable Links', 1)).toBe('Kopiuj udostępnialny link'); | ||||
| 		expect(_n('Copy Shareable Link', 'Copy Shareable Links', 2)).toBe('Kopiuj udostępnialne linki'); | ||||
| 		expect(_n('Copy Shareable Link', 'Copy Shareable Links', 5)).toBe('Kopiuj udostępnialne linki'); | ||||
| 	}); | ||||
|  | ||||
| 	it('should parse the plural form', async () => { | ||||
| 		const pluralForms = [ | ||||
| 			'nplurals=1; plural=0;', | ||||
| 			'nplurals=2; plural=(n != 0);', | ||||
| 			'nplurals=2; plural=(n != 1);', | ||||
| 			'nplurals=2; plural=(n > 1);', | ||||
| 			'nplurals=2; plural=(n%10!=1 || n%100==11);', | ||||
| 			'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);', | ||||
| 			'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);', | ||||
| 			'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);', | ||||
| 			'nplurals=3; plural=(n==0 ? 0 : n==1 ? 1 : 2);', | ||||
| 			'nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2);', | ||||
| 			'nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);', | ||||
| 			'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;', | ||||
| 			'nplurals=3; plural=(n==1) ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;', | ||||
| 			'nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);', | ||||
| 			'nplurals=4; plural=(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3);', | ||||
| 			'nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3;', | ||||
| 			'nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;', | ||||
| 			'nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3;', | ||||
| 			'nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>6 && n<11) ? 3 : 4;', | ||||
| 			'nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);', | ||||
| 		]; | ||||
|  | ||||
| 		const pluralValues = [ | ||||
| 			[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | ||||
| 			[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], | ||||
| 			[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], | ||||
| 			[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], | ||||
| 			[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1], | ||||
| 			[2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1], | ||||
| 			[2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1], | ||||
| 			[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2], | ||||
| 			[0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], | ||||
| 			[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2], | ||||
| 			[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2], | ||||
| 			[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], | ||||
| 			[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2], | ||||
| 			[0, 1, 2, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | ||||
| 			[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3], | ||||
| 			[3, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], | ||||
| 			[2, 0, 1, 2, 2, 2, 2, 2, 3, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], | ||||
| 			[3, 0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], | ||||
| 			[4, 0, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], | ||||
| 			[0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], | ||||
| 		]; | ||||
|  | ||||
| 		for (let index = 0; index < pluralForms.length; index++) { | ||||
| 			const form = pluralForms[index]; | ||||
| 			const pluralFn = parsePluralForm(form); | ||||
| 			for (let i = 0; i < 128; ++i) { | ||||
| 				expect(pluralValues[index][i]).toBe(pluralFn(i)); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -7,6 +7,7 @@ interface StringToStringMap { | ||||
| interface CodeToCountryMap { | ||||
| 	[key: string]: string[]; | ||||
| } | ||||
| type ParsePluralFormFunction = (n: number)=> number; | ||||
|  | ||||
| const codeToLanguageE_: StringToStringMap = {}; | ||||
| codeToLanguageE_['aa'] = 'Afar'; | ||||
| @@ -436,12 +437,51 @@ const codeToCountry_: CodeToCountryMap = { | ||||
| let supportedLocales_: any = null; | ||||
| let localeStats_: any = null; | ||||
|  | ||||
| const loadedLocales_: any = {}; | ||||
| const loadedLocales_: Record<string, Record<string, string[]>> = {}; | ||||
|  | ||||
| const pluralFunctions_: Record<string, ParsePluralFormFunction> = {}; | ||||
|  | ||||
| const defaultLocale_ = 'en_GB'; | ||||
|  | ||||
| let currentLocale_ = defaultLocale_; | ||||
|  | ||||
| // Copied from https://github.com/eugeny-dementev/parse-gettext-plural-form | ||||
| // along with the tests | ||||
| export const parsePluralForm = (form: string): ParsePluralFormFunction => { | ||||
| 	const pluralFormRegex = /^(\s*nplurals\s*=\s*[0-9]+\s*;\s*plural\s*=\s*(?:\s|[-?|&=!<>+*/%:;a-zA-Z0-9_()])+)$/m; | ||||
|  | ||||
| 	if (!pluralFormRegex.test(form)) throw new Error(`Plural-Forms is invalid: ${form}`); | ||||
|  | ||||
| 	if (!/;\s*$/.test(form)) { | ||||
| 		form += ';'; | ||||
| 	} | ||||
|  | ||||
| 	const code = [ | ||||
| 		'var plural;', | ||||
| 		'var nplurals;', | ||||
| 		form, | ||||
| 		'return (plural === true ? 1 : plural ? plural : 0);', | ||||
| 	].join('\n'); | ||||
|  | ||||
| 	// eslint-disable-next-line no-new-func -- There's a regex to check the form but it's still slighlty unsafe, eventually we should automatically generate all the functions in advance in build-translations.ts | ||||
| 	return (new Function('n', code)) as ParsePluralFormFunction; | ||||
| }; | ||||
|  | ||||
| const getPluralFunction = (lang: string) => { | ||||
| 	if (!(lang in pluralFunctions_)) { | ||||
| 		const locale = closestSupportedLocale(lang); | ||||
| 		const stats = localeStats()[locale]; | ||||
|  | ||||
| 		if (!stats.pluralForms) { | ||||
| 			pluralFunctions_[lang] = null; | ||||
| 		} else { | ||||
| 			pluralFunctions_[lang] = parsePluralForm(stats.pluralForms); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return pluralFunctions_[lang]; | ||||
| }; | ||||
|  | ||||
| function defaultLocale() { | ||||
| 	return defaultLocale_; | ||||
| } | ||||
| @@ -589,18 +629,45 @@ function _(s: string, ...args: any[]): string { | ||||
| } | ||||
|  | ||||
| function _n(singular: string, plural: string, n: number, ...args: any[]) { | ||||
| 	if (n > 1) return _(plural, ...args); | ||||
| 	return _(singular, ...args); | ||||
| 	if (['en_GB', 'en_US'].includes(currentLocale_)) { | ||||
| 		if (n > 1) return _(plural, ...args); | ||||
| 		return _(singular, ...args); | ||||
| 	} else { | ||||
| 		const pluralFn = getPluralFunction(currentLocale_); | ||||
| 		const stringIndex = pluralFn ? pluralFn(n) : 0; | ||||
| 		const strings = localeStrings(currentLocale_); | ||||
| 		const result = strings[singular]; | ||||
|  | ||||
| 		let translatedString = ''; | ||||
| 		if (result === undefined || !result.join('')) { | ||||
| 			translatedString = singular; | ||||
| 		} else { | ||||
| 			translatedString = stringIndex < result.length ? result[stringIndex] : result[0]; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			return sprintf(translatedString, ...args); | ||||
| 		} catch (error) { | ||||
| 			return `${translatedString} ${args.join(', ')} (Translation error: ${error.message})`; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const stringByLocale = (locale: string, s: string, ...args: any[]): string => { | ||||
| 	const strings = localeStrings(locale); | ||||
| 	let result = strings[s]; | ||||
| 	if (result === '' || result === undefined) result = s; | ||||
| 	const result = strings[s]; | ||||
| 	let translatedString = ''; | ||||
|  | ||||
| 	if (result === undefined || !result.join('')) { | ||||
| 		translatedString = s; | ||||
| 	} else { | ||||
| 		translatedString = result[0]; | ||||
| 	} | ||||
|  | ||||
| 	try { | ||||
| 		return sprintf(result, ...args); | ||||
| 		return sprintf(translatedString, ...args); | ||||
| 	} catch (error) { | ||||
| 		return `${result} ${args.join(', ')} (Translation error: ${error.message})`; | ||||
| 		return `${translatedString} ${args.join(', ')} (Translation error: ${error.message})`; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| // Dependencies:
 | ||||
| //
 | ||||
| // sudo apt install gettext sudo apt install translate-toolkit
 | ||||
| @@ -7,34 +5,36 @@ | ||||
| // gettext v21+ is required as versions before that have bugs when parsing
 | ||||
| // JavaScript template strings which means we would lose translations.
 | ||||
| 
 | ||||
| const rootDir = `${__dirname}/../..`; | ||||
| import markdownUtils from '@joplin/lib/markdownUtils'; | ||||
| import { translationExecutablePath, removePoHeaderDate, mergePotToPo, parsePoFile, parseTranslations, TranslationStatus } from './utils/translation'; | ||||
| import { execCommand, isMac, insertContentIntoFile, filename, dirname, fileExtension } from './tool-utils.js'; | ||||
| import { countryDisplayName, countryCodeOnly } from '@joplin/lib/locale'; | ||||
| import { readdirSync, writeFileSync } from 'fs'; | ||||
| import { readFile } from 'fs/promises'; | ||||
| import { copy, mkdirpSync, remove } from 'fs-extra'; | ||||
| import { GettextExtractor, JsExtractors } from 'gettext-extractor'; | ||||
| 
 | ||||
| const markdownUtils = require('@joplin/lib/markdownUtils').default; | ||||
| const fs = require('fs-extra'); | ||||
| const { translationExecutablePath, removePoHeaderDate, mergePotToPo, parsePoFile, parseTranslations } = require('./utils/translation'); | ||||
| const rootDir = `${__dirname}/../..`; | ||||
| const localesDir = `${__dirname}/locales`; | ||||
| const libDir = `${rootDir}/packages/lib`; | ||||
| const { execCommand, isMac, insertContentIntoFile, filename, dirname, fileExtension } = require('./tool-utils.js'); | ||||
| const { countryDisplayName, countryCodeOnly } = require('@joplin/lib/locale'); | ||||
| 
 | ||||
| const { GettextExtractor, JsExtractors } = require('gettext-extractor'); | ||||
| 
 | ||||
| function serializeTranslation(translation) { | ||||
| function serializeTranslation(translation: string) { | ||||
| 	const output = parseTranslations(translation); | ||||
| 	return JSON.stringify(output, Object.keys(output).sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : +1), ' '); | ||||
| 	return JSON.stringify(output, Object.keys(output).sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : +1), '\t'); | ||||
| } | ||||
| 
 | ||||
| function saveToFile(filePath, data) { | ||||
| 	fs.writeFileSync(filePath, data); | ||||
| function saveToFile(filePath: string, data: string) { | ||||
| 	writeFileSync(filePath, data); | ||||
| } | ||||
| 
 | ||||
| async function buildLocale(inputFile, outputFile) { | ||||
| async function buildLocale(inputFile: string, outputFile: string) { | ||||
| 	const r = await parsePoFile(inputFile); | ||||
| 	const translation = serializeTranslation(r); | ||||
| 	saveToFile(outputFile, translation); | ||||
| 	return { headers: r.headers }; | ||||
| } | ||||
| 
 | ||||
| async function createPotFile(potFilePath) { | ||||
| async function createPotFile(potFilePath: string) { | ||||
| 	const excludedDirs = [ | ||||
| 		'./.git/*', | ||||
| 		'./.github/*', | ||||
| @@ -82,7 +82,7 @@ async function createPotFile(potFilePath) { | ||||
| 	// basename, such as "exmaple.js", and "example.ts", we only keep the file
 | ||||
| 	// with ".ts" extension (since the .js should be the compiled file).
 | ||||
| 
 | ||||
| 	const toProcess = {}; | ||||
| 	const toProcess: Record<string, string> = {}; | ||||
| 
 | ||||
| 	for (const file of files) { | ||||
| 		if (!file) continue; | ||||
| @@ -172,7 +172,7 @@ async function createPotFile(potFilePath) { | ||||
| 	await removePoHeaderDate(potFilePath); | ||||
| } | ||||
| 
 | ||||
| function buildIndex(locales, stats) { | ||||
| function buildIndex(locales: string[], stats: TranslationStatus[]) { | ||||
| 	const output = []; | ||||
| 	output.push('var locales = {};'); | ||||
| 	output.push('var stats = {};'); | ||||
| @@ -196,10 +196,10 @@ function buildIndex(locales, stats) { | ||||
| 	return output.join('\n'); | ||||
| } | ||||
| 
 | ||||
| function availableLocales(defaultLocale) { | ||||
| function availableLocales(defaultLocale: string) { | ||||
| 	const output = [defaultLocale]; | ||||
| 	// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
 | ||||
| 	fs.readdirSync(localesDir).forEach((path) => { | ||||
| 	readdirSync(localesDir).forEach((path) => { | ||||
| 		if (fileExtension(path) !== 'po') return; | ||||
| 		const locale = filename(path); | ||||
| 		if (locale === defaultLocale) return; | ||||
| @@ -208,7 +208,7 @@ function availableLocales(defaultLocale) { | ||||
| 	return output; | ||||
| } | ||||
| 
 | ||||
| function extractTranslator(regex, poContent) { | ||||
| function extractTranslator(regex: RegExp, poContent: string) { | ||||
| 	const translatorMatch = poContent.match(regex); | ||||
| 	let translatorName = ''; | ||||
| 
 | ||||
| @@ -225,13 +225,13 @@ function extractTranslator(regex, poContent) { | ||||
| 	return translatorName; | ||||
| } | ||||
| 
 | ||||
| function translatorNameToMarkdown(translatorName) { | ||||
| function translatorNameToMarkdown(translatorName: string) { | ||||
| 	const matches = translatorName.match(/^(.*?)\s*\((.*)\)$/); | ||||
| 	if (!matches) return translatorName; | ||||
| 	return `[${markdownUtils.escapeTitleText(matches[1])}](mailto:${markdownUtils.escapeLinkUrl(matches[2])})`; | ||||
| } | ||||
| 
 | ||||
| async function translationStatus(isDefault, poFile) { | ||||
| async function translationStatus(isDefault: boolean, poFile: string): Promise<TranslationStatus> { | ||||
| 	// "apt install translate-toolkit" to have pocount
 | ||||
| 	let pocountPath = 'pocount'; | ||||
| 	if (isMac()) pocountPath = translationExecutablePath('pocount'); | ||||
| @@ -248,7 +248,7 @@ async function translationStatus(isDefault, poFile) { | ||||
| 	const untranslatedCount = Number(untranslatedMatches[1]); | ||||
| 
 | ||||
| 	let translatorName = ''; | ||||
| 	const content = await fs.readFile(poFile, 'utf-8'); | ||||
| 	const content = await readFile(poFile, 'utf-8'); | ||||
| 
 | ||||
| 	translatorName = extractTranslator(/Last-Translator:\s*?(.*)/, content); | ||||
| 	if (!translatorName) { | ||||
| @@ -276,7 +276,7 @@ async function translationStatus(isDefault, poFile) { | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| function flagImageUrl(locale) { | ||||
| function flagImageUrl(locale: string) { | ||||
| 	const baseUrl = 'https://joplinapp.org/images/flags'; | ||||
| 	if (locale === 'ar') return `${baseUrl}/country-4x3/arableague.png`; | ||||
| 	if (locale === 'eu') return `${baseUrl}/es/basque_country.png`; | ||||
| @@ -292,11 +292,11 @@ function flagImageUrl(locale) { | ||||
| 	return `${baseUrl}/country-4x3/${countryCodeOnly(locale).toLowerCase()}.png`; | ||||
| } | ||||
| 
 | ||||
| function poFileUrl(locale) { | ||||
| function poFileUrl(locale: string) { | ||||
| 	return `https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/${locale}.po`; | ||||
| } | ||||
| 
 | ||||
| function translationStatusToMdTable(status) { | ||||
| function translationStatusToMdTable(status: TranslationStatus[]) { | ||||
| 	const output = []; | ||||
| 	output.push([' ', 'Language', 'Po File', 'Last translator', 'Percent done'].join('  |  ')); | ||||
| 	output.push(['---', '---', '---', '---', '---'].join('|')); | ||||
| @@ -308,7 +308,7 @@ function translationStatusToMdTable(status) { | ||||
| 	return output.join('\n'); | ||||
| } | ||||
| 
 | ||||
| async function updateReadmeWithStats(stats) { | ||||
| async function updateReadmeWithStats(stats: TranslationStatus[]) { | ||||
| 	await insertContentIntoFile( | ||||
| 		`${rootDir}/README.md`, | ||||
| 		'<!-- LOCALE-TABLE-AUTO-GENERATED -->\n', | ||||
| @@ -317,12 +317,12 @@ async function updateReadmeWithStats(stats) { | ||||
| 	); | ||||
| } | ||||
| 
 | ||||
| async function translationStrings(poFilePath) { | ||||
| async function translationStrings(poFilePath: string) { | ||||
| 	const r = await parsePoFile(poFilePath); | ||||
| 	return Object.keys(r.translations['']); | ||||
| } | ||||
| 
 | ||||
| function deletedStrings(oldStrings, newStrings) { | ||||
| function deletedStrings(oldStrings: string[], newStrings: string[]) { | ||||
| 	const output = []; | ||||
| 	for (const s1 of oldStrings) { | ||||
| 		if (newStrings.includes(s1)) continue; | ||||
| @@ -342,7 +342,7 @@ async function main() { | ||||
| 
 | ||||
| 	if (missingStringsCheckOnly) { | ||||
| 		tempPotFilePath = `${localesDir}/joplin-temp-${Math.floor(Math.random() * 10000000)}.pot`; | ||||
| 		await fs.copy(potFilePath, tempPotFilePath); | ||||
| 		await copy(potFilePath, tempPotFilePath); | ||||
| 		potFilePath = tempPotFilePath; | ||||
| 	} | ||||
| 
 | ||||
| @@ -359,7 +359,7 @@ async function main() { | ||||
| 
 | ||||
| 	console.info(`Updated pot file. Total strings: ${oldPotStatus.untranslatedCount} => ${newPotStatus.untranslatedCount}`); | ||||
| 
 | ||||
| 	if (tempPotFilePath) await fs.remove(tempPotFilePath); | ||||
| 	if (tempPotFilePath) await remove(tempPotFilePath); | ||||
| 
 | ||||
| 	const deletedCount = oldPotStatus.untranslatedCount - newPotStatus.untranslatedCount; | ||||
| 	if (deletedCount >= 5) { | ||||
| @@ -379,7 +379,7 @@ async function main() { | ||||
| 
 | ||||
| 	await execCommand(`cp "${potFilePath}" ` + `"${localesDir}/${defaultLocale}.po"`); | ||||
| 
 | ||||
| 	fs.mkdirpSync(jsonLocalesDir, 0o755); | ||||
| 	mkdirpSync(jsonLocalesDir, 0o755); | ||||
| 
 | ||||
| 	const stats = []; | ||||
| 
 | ||||
| @@ -392,9 +392,10 @@ async function main() { | ||||
| 		const poFilePäth = `${localesDir}/${locale}.po`; | ||||
| 		const jsonFilePath = `${jsonLocalesDir}/${locale}.json`; | ||||
| 		if (locale !== defaultLocale) await mergePotToPo(potFilePath, poFilePäth); | ||||
| 		await buildLocale(poFilePäth, jsonFilePath); | ||||
| 		const { headers } = await buildLocale(poFilePäth, jsonFilePath); | ||||
| 
 | ||||
| 		const stat = await translationStatus(defaultLocale === locale, poFilePäth); | ||||
| 		stat.pluralForms = headers['Plural-Forms']; | ||||
| 		stat.locale = locale; | ||||
| 		stat.languageName = countryDisplayName(locale); | ||||
| 		stats.push(stat); | ||||
| @@ -2,7 +2,16 @@ import { execCommand, isMac } from '../tool-utils'; | ||||
| import { existsSync, readFile } from 'fs-extra'; | ||||
| const gettextParser = require('gettext-parser'); | ||||
|  | ||||
| export type Translations = Record<string, string>; | ||||
| export interface TranslationStatus { | ||||
| 	locale?: string; | ||||
| 	languageName?: string; | ||||
| 	translatorName: string; | ||||
| 	percentDone: number; | ||||
| 	untranslatedCount: number; | ||||
| 	pluralForms?: string; | ||||
| } | ||||
|  | ||||
| export type Translations = Record<string, string[]>; | ||||
|  | ||||
| export const removePoHeaderDate = async (filePath: string) => { | ||||
| 	let sedPrefix = 'sed -i'; | ||||
| @@ -59,14 +68,14 @@ export const parseTranslations = (gettextTranslations: any) => { | ||||
| 			if (!translations.hasOwnProperty(n)) continue; | ||||
| 			if (n === '') continue; | ||||
| 			const t = translations[n]; | ||||
| 			let translated = ''; | ||||
| 			let translated: string[] = []; | ||||
| 			if (t.comments && t.comments.flag && t.comments.flag.indexOf('fuzzy') >= 0) { | ||||
| 				// Don't include fuzzy translations | ||||
| 			} else { | ||||
| 				translated = t['msgstr'][0]; | ||||
| 				translated = t['msgstr']; | ||||
| 			} | ||||
|  | ||||
| 			if (translated) output[n] = translated; | ||||
| 			if (translated.length) output[n] = translated; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ describe('applyTranslations', () => { | ||||
| 			{ | ||||
| 				html: '<div><span translate>Translate me</span></div>', | ||||
| 				translations: { | ||||
| 					'Translate me': 'Traduis moi', | ||||
| 					'Translate me': ['Traduis moi'], | ||||
| 				}, | ||||
| 				htmlTranslated: '<div>\n<span translate>\nTraduis moi\n</span>\n</div>', | ||||
| 			}, | ||||
| @@ -19,14 +19,14 @@ describe('applyTranslations', () => { | ||||
| 			{ | ||||
| 				html: '<h1 translate class="text-center">\nFree your <span class="frame-bg frame-bg-blue">notes</span>\n</h1>', | ||||
| 				translations: { | ||||
| 					'Free your <span class="frame-bg frame-bg-blue">notes</span>': 'Libérez vos <span class="frame-bg frame-bg-blue">notes</span>', | ||||
| 					'Free your <span class="frame-bg frame-bg-blue">notes</span>': ['Libérez vos <span class="frame-bg frame-bg-blue">notes</span>'], | ||||
| 				}, | ||||
| 				htmlTranslated: '<h1 translate class="text-center">\nLibérez vos <span class="frame-bg frame-bg-blue">notes</span>\n</h1>', | ||||
| 			}, | ||||
| 			{ | ||||
| 				html: '<div translate>Save <span class="frame-bg frame-bg-blue">web pages</span> <br />as notes</div>', | ||||
| 				translations: { | ||||
| 					'Save <span class="frame-bg frame-bg-blue">web pages</span> <br>as notes': 'Sauvegardez vos <span class="frame-bg frame-bg-blue">pages web</span> <br>en notes', | ||||
| 					'Save <span class="frame-bg frame-bg-blue">web pages</span> <br>as notes': ['Sauvegardez vos <span class="frame-bg frame-bg-blue">pages web</span> <br>en notes'], | ||||
| 				}, | ||||
| 				htmlTranslated: '<div translate>\nSauvegardez vos <span class="frame-bg frame-bg-blue">pages web</span> <br>en notes\n</div>', | ||||
| 			}, | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { unique } from '@joplin/lib/ArrayUtils'; | ||||
| import { attributesHtml, isSelfClosingTag } from '@joplin/renderer/htmlUtils'; | ||||
| import { Translations } from '../../utils/translation'; | ||||
| const Entities = require('html-entities').AllHtmlEntities; | ||||
| const htmlentities = new Entities().encode; | ||||
| const htmlparser2 = require('@joplin/fork-htmlparser2'); | ||||
| @@ -15,7 +16,7 @@ const trimHtml = (content: string) => { | ||||
| 		.replace(/\t+$/, ''); | ||||
| }; | ||||
|  | ||||
| const findTranslation = (englishString: string, translations: Record<string, string>): string => { | ||||
| const findTranslation = (englishString: string, translations: Translations): string => { | ||||
| 	const stringsToTry = unique([ | ||||
| 		englishString, | ||||
| 		englishString.replace(/<br\/>/gi, '<br>'), | ||||
| @@ -26,7 +27,8 @@ const findTranslation = (englishString: string, translations: Record<string, str | ||||
| 	]) as string[]; | ||||
|  | ||||
| 	for (const stringToTry of stringsToTry) { | ||||
| 		if (translations[stringToTry]) return translations[stringToTry]; | ||||
| 		// Note that we don't currently support plural forms for the website | ||||
| 		if (translations[stringToTry] && translations[stringToTry].length) return translations[stringToTry][0]; | ||||
| 	} | ||||
|  | ||||
| 	return englishString; | ||||
| @@ -38,7 +40,7 @@ const encodeHtml = (decodedText: string): string => { | ||||
| 		.replace(/{{> /gi, '{{> '); // Don't break Mustache partials | ||||
| }; | ||||
|  | ||||
| export default (html: string, _languageCode: string, translations: Record<string, string>) => { | ||||
| export default (html: string, _languageCode: string, translations: Translations) => { | ||||
| 	const output: string[] = []; | ||||
|  | ||||
| 	interface State { | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| import { mkdirp, readFile, writeFile } from 'fs-extra'; | ||||
| import { dirname } from 'path'; | ||||
| import { Translations } from '../../utils/translation'; | ||||
| import applyTranslations from './applyTranslations'; | ||||
|  | ||||
| export default async (englishFilePath: string, translatedFilePath: string, languageCode: string, translations: Record<string, string>) => { | ||||
| export default async (englishFilePath: string, translatedFilePath: string, languageCode: string, translations: Translations) => { | ||||
| 	let content = await readFile(englishFilePath, 'utf8'); | ||||
| 	content = content.replace('<html lang="en-gb">', `<html lang="${languageCode}">`); | ||||
| 	const translatedContent = await applyTranslations(content, languageCode, translations); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Plan, StripePublicConfig } from '@joplin/lib/utils/joplinCloud'; | ||||
| import { Sponsors } from '../../utils/loadSponsors'; | ||||
| import { Translations } from '../../utils/translation'; | ||||
| import { OpenGraphTags } from './openGraph'; | ||||
|  | ||||
| export enum Env { | ||||
| @@ -8,7 +9,7 @@ export enum Env { | ||||
| } | ||||
|  | ||||
| export interface Locale { | ||||
| 	htmlTranslations: Record<string, string>; | ||||
| 	htmlTranslations: Translations; | ||||
| 	lang: string; | ||||
| 	pathPrefix: string; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user